이 도움말 파일의 저작권은 진경소프트가 가집니다.

http://kepler.kyungpook.ac.kr

seojt@comeng.ce.kyungpook.ac.kr

seojt@chollian.net


0. 들어가기 전에

  이 책의 머리말을 지금 읽고 있는 독자들은 타의나 자의에 의해서, C++를
익혀야 하는 상황의 독자일 것이다. 그것이 학기 수업일수도 있고, 막연한 호
기심이나 취업을 위한 준비가 될 수도 있다. 어쨌던 한 가지 확실한 것은 독
자들은 지금 C++ 학습을 위한 최선의 선택을 하였다는 것이다!

  이 책의 첫 페이지를 쓸 때는 아직 C++ 표준이 정해지지 않은 상태였다.
하지만, 지금은 ISO에 의해 표준이 정해졌다. 1997년 11월 14일 아침 10시 30
분경 이었다!

  "먼저 C로 시작한다. 그리곤 자연스럽게 C++로 넘어간다." 라는 진부한 발
상은 틀린 것이다. C++부터 시작해야 한다. 하지만, C를 설명해야 한다. 이 책
은 C++을 설명하면서 C에서 달라진 점을 상세히 설명한다. 그리곤 이 책의
어느 지점부터는 C++에 대해서만 이야기한다(당신도 모르는 사이). 1996년 제
안된 초안(draft)에 제시된 많은 사항들이 C++ 표준에서 제외되었다. 하지만,
필요할 때는 초안의 사항들을 함께 설명하였다(재미있고 기발한 아이디어가
얼마나 많은가!). 사실상 이 책은 AT&T의 FTP 사이트(site) -
ftp://ftp.research.att.com - 에서 얻은 1996년 초안을 많이 참고하였다. 함께
제공되는 CD-ROM의 초안 PDF(portable document format) 파일을 참고하기
바란다.

  초안이 C++교재가 될 수 없다(그것은 전문용어로 가득 찬 700페이지 분량
이다!). 전문적인 용어들이 C++ 초심자들을 당황케 하기 때문이다. C++전문가
조차 초안의 CFG(context free grammar)를 읽는 것은 지루한 일이다. 하지
만, 초안에서 필요한 전문용어들을 되도록 빠뜨리지 않고 설명하도록 노력하
였다. 하지만, 필요하다면 무시해도 좋다. 그렇지 않으면 전문용어의 홍수 속
에 빠져 버릴지도 모른다.

  처음에는 책을 두 부분으로 나누어 1부에서는 C++에 대해서 설명하고, 2부
에서는 '문제 해결 방법'을 설명할 생각이었으나, 내용이 방대해 지는 바람에
'문제 해결 방법'은 다른 책으로 편집하게 되었다. 이 책을 완파(완독(entire
reading)이 아니라 완파(entire understanding)이다)한 독자는 이 책의 후속권
인 "Problem Kernel"를 반드시 읽어보기 바란다. 모르면 영원히 모를지도
모르고 있을 소중한 내용들이 2부의 책에 들어있다.

Ritchie가 말하기를 C가 있으라, 그러자 C가 있었다. 그것이 사람들이 보기에
좋았다. 하지만, C가 독처하는 것은 좋지 않았다. 후에 Bjarne이 생각했다.
'내가 C를 돕는 베필을 지으리라.' 그래서 C의 형상을 따라, 클래스(class)를
만들었다.

  독자들의 믿지 않을지도 모르지만, 위의 문장은 사실이다. 위의 문장은 원래
는 다음과 같은 아인시타인(Albert Einstein)을 주제로 한 과학도서에서 힌트
를 얻은 것이다.

태초에 뉴튼(Sir Isaac Newton)이 과학법칙을 창조했다. 모든 과학법칙이
혼돈과 공허속에 있을 때, 뉴튼이 말하기를 과학법칙이 있으라, 그러자
과학법칙이 있었다. 그것이 사람들의 보기에 좋았다. 하지만, 과학법칙이 혼자
독처하는 것은 좋지 않았다. 후에 아인시타인이 생각했다. '내가 과학법칙을
돕는 베필을 지으리라.' 그래서 과학법칙의 형상을 따라, 상대성 이론(the
relativity)을 만들었다.

  독자들이 또 믿지 않을지도 모르지만, 위의 문장은 사실이다. 위의 문장은
원래는 다음의 성경구절을 응용한 것이다.

태초에 하나님이 천지를 창조하시니라. 땅이 혼돈하고 공허하며 흑암이 깊음
위에 있고 하나님의 신은 수면에 운행하시니라. 하나님이 가라사대 빛이 있으
라 하시매 빛이 있었고, 그 빛이 하나님의 보시기에 좋았더라. ... 여호와 하
나님이 가라사대 사람의 독처하는 것이 좋지 못하니 내가 그를 위하여 돕는
배필을 지으리라 하시니라.

  위의 문장은 독자들이 가장 믿기 어려워하는 사실이겠지만, 역시 사실이다!

  왜 언어의 이름이 C++인지 물어보는 사람이 가끔 있다. 다음은 여기에 대
한 Stroustrup의 대답이다(그의 홈페이지인 http://www.research.att.com/~bs/)
에서 얻은 내용이다). 그는 C++을 처음으로 설계한 사람이다.

왜 언어의 이름이 C++죠?

  처음 몇 년 동안은 저는 저의 언어를 "클래스를 가진 C(C with Classes)"라
고 불렀습니다. 그러나 사람들은 "클래스를 가진 C"를 "새로운 C(new C)"로
불렀습니다. 그것은 원래의 C를 "평범한 C", "그냥 C(straight C)" 그리고 "옛
날 C"로 불리도록 했습니다. 특별히 마지막 이름 C는 모욕적인 것으로 간주
되었으므로(역주: Bjarne은 자신이 만든 언어가 C보다 훨씬 강력함을 나타내
고자 했다), 예의가 필요했으며, 혼돈을 피하기 위해 저는 새로운 이름을 찾게
되었습니다. 저는 C++를 선택했습니다. 왜냐하면, 짧고, 훌륭한 해석을 가지
며, "형용사 C"형태가 아니었기 때문입니다. C언어에서 ++는 문맥에 따라 "다
음(next)", "계승자(successor)" 혹은 "증가(increment)"로 읽힙니다. 물론 실제
발음은 "뿔뿔(plus plus)"입니다(역주: '뿔뿔'이 콩글리쉬이기는 하지만, 일반적
으로 이렇게 읽으므로, 일반성을 무시할 수 없다). C++과 ++C는 농담과 재미
를 위한 비옥한 소스입니다. 물론 이러한 농담은 이름이 선택되기 전에 대부
분 알려졌습니다(역주: 필자는 이러한 농담을 아는 바 없지만, 아마 정수 변수
C를 1 증가시키기 위해서 C++; 라고 사용하는 그런 류의 농담일 것이다).
C++이란 이름은 Rick Mascitti에 의해 처음 제안되었습니다. 그리고 1983년
12월에 처음으로 사용되었습니다.

  D라고 불리는 많은 언어가 있습니다. 이름에 관한 퀴즈(trivia)를 보기 위해
서 저의 저서 Design and Evolution를 보십시오.

C++와 C, 무엇이 다른가?

  여기에 관한 대답은 초안(draft)에 잘 나타나 있다.

  「C++은 C에 기초한 범용 언어이다. C가 제공하는 기능 외에 C++는 추가
적인 데이터 타입, 클래스, 템플릿, 예외처리, 이름공간, 인라인 함수, 연산
자 오버로딩, 함수이름 오버로딩, 참조, 메모리관리 연산자와 추가적인 라
이브러리 기능을 제공한다」

환경

  가끔식 책이 만들어지고, 프로그램이 컴파일된 환경을 궁금해하는 독자들이
있다. 필자의 시스템 사양은 다음과 같다.

    □ Pentium™ 90, 32MegaByte Main Memory, 3GigaByte Hard Disk

    □ 17inch SAMTRON 컬러 모니터

    □ HWP97™ 워드 프로세서

    □ Visio™ 드로잉 툴(drawing tool), PaintShopPro™ 그래픽 툴

  프로그램 컴파일에 사용한 툴(tool)은 다음과 같다.

    □ Borland C++ 3.1, 4.5

    □ C++ Builder 3.0

    □ Visual C++ 5.0, 6.0

    필자가 지금 돈을 목적으로 책을 만든다는 생각이 지배적이었던 어느 중간
시기, 책을 만드는 것을 그만두려는 생각을 한 적이 있다. 하지
만, 끝까지 책을 완성하게 해 주신 하나님(The God)께 감사드
린다. 별로 되지도 않겠지만, 책에서 생기는 이익금은 사업을
위해 돈을 지원하기로 약속한 나의 사랑하는 미경의 오빠에게
조금, 나머지는 모두 가난한 이들을 위해 사용할 것이다.

1999년 3월 서진택

seojt@comeng.ce.kyungpook.ac.kr, seojt@chollian.net

http://kepler.kyungpook.ac.kr


c언어 전부

2. 수학 함수를 C 함수로

  C는 함수(function)들의 집합이다. 비록 약간의 차이는 있지만, C의 함수는
수학에서 사용하는 함수의 개념과 유사하다. 그러므로 C로 프로그램을 코딩
(coding)한다는 것은, 바로 프로그램에서 필요로 하는 함수를 만들어 주는 것
이다. 그러므로 독자들은 수학함수가 주어졌을 때 이를 C함수로 바꿀 수 있어
야 한다.

  다음과 같은 4개의 수학함수가 주어졌을 때 이를 C의 함수로 바꾸는 과정
을 살펴보자.

  (1) f(x)=x2+5

  (2) g(x,y)=f(x)+y

  (3) y=3

  (4) l(x,y)={원점에서 (x,y)까지 선을 그린다}

  먼저, 필요한 용어들을 정의하자. 함수 f(x)=x2+5 에서 f는 함수 이름
(function name)이다. 함수가 받는 파라미터(parameter; argument)는 x인
데, 함수 이름 다음에 괄호를 써서 나타낸다1). 함수가 하는 일은 =다음에 정

  1) 파라미터의 개수가 2개 이상일 때는 파라미터 리스트(parameter list)라 하는데, 콤마

    로 구분하여 열거한다. 예를 들면 x,y,z 처럼 나타낸다.

의하는데, 이를 함수 몸체(function body)라 한다. =의 의미는 f(x)와 x2+5가
같다는 의미이다. 그러므로 f(x)라고 써야 할 자리에 x2+5를 적어도 된다. 예
를 들면, f(2)는 22+5를 의미한다.

  특별하게 함수를 정의(define)하는 쪽에서 파라미터를 형식 파라미터
(formal parameter)라 한다. 이 말은 실제의 값(value)은 모르지만 그 값을
나타내는 형식적인 표기로 적어 놓은 변수임을 나타낸다. 예를 들어 사용자가
f(2)처럼 함수 f를 호출(call)2)하면 형식 파라미터 x의 실제 값은 2가 되어 함
  2) 정의된 함수를 사용하는 것을 '함수를 호출한다'라고 한다.

수의 몸체가 평가(evaluate)된다.

    x=2

    y=3

    i=f(y)

  위와 같은 일련의 문장이 실행되었을 때, i의 값은 얼마일까? 'f(x)=x2+5 이
며, x의 값은 2이므로 i는 9이다.' 라고 계산해서는 안 된다. f를 호출할 때 형
식 파라미터 x의 값은 y의 값인 3으로 치환(substitution)되어 평가되므로, 결
과는 14가 되어 i의 값은 14가 된다3). 이때 실제 x에 치환된 값을 실제 파라
  3) i가 14가 되는 것에 주의하라. 함수 f가 값을 돌려주지(return) 않는다면 i는 값을 가질

    수 없다. C에서는 문장(statement)이 값을 가질 수 도, 가지지 않을 수도 있는데, 값을

    가지는 문장을 표현식(expression)이라 한다. 예를 들면 다음은 표현식이다.

3+2

      다음은 표현식이 아니다.

"날씨 맑음"

미터(actual parameter)라 한다. 독자들은 실제 파라미터와 형식 파라미터의
차이를 명확히 이해하고 있어야 한다. 파라미터를 전달받는 쪽 - 정의하는 쪽
- 에서 선언한 변수를 형식 파라미터라 한다. 반면에 파라미터를 전달하는 쪽
- 호출하는 쪽 - 에서 사용한 변수를 실제 파라미터라 한다.

  그렇다면 독자들은 첫번째 규칙을 이해할 준비가 되었다.

"Formal parameter와 actual parameter는 변수의 이름이 달라도 된다."

  그러면 차례대로 위의 함수들을 C의 함수로 바꾸는 과정을 살펴보자.

f(x)=x2+5

  1단계. 함수의 이름과 파라미터를 그대로 써 준다.

f(x)=x2+5 → f(    x)

  2단계. 함수의 몸체 부분을 여는 브레이스(open brace; {)와 닫는 브레이스
(close brace; {)안에 써 준다4).

  4) 후에 정의하게 되겠지만 여는 브레이스와 닫는 브레이스 안에 포함된 일련의 문장들

    을 - 브레이스를 포함하여 - 블락(block)이라고 한다.

f(x)=x2+5 → f(    x) { x*x+5 }

  C에는 제곱(power of 2) 연산자(operator)가 없으므로 x*x로 나타내었다5).
  5) 전산학에서는 소문자 x와 곱하기 ×를 구분하기 위해 곱하기 대신에 *(asterisk)를 사

    용한다.

그리고 보통의 C프로그래머들은 보기 좋게 위의 C함수를 다음과 같이 나타낸
다. 즉 다른 줄에 나타내어 읽기 좋게 한다6).

  6) 의미 있는 문자 - 이를 토큰(token)이라 한다 - 사이에는 몇 개의 공백(space) 문자,

    탭(tab) 문자, 줄 바꿈(return) 문자가 와도 상관이 없다. 이들 3개의 문자를 흰 공백

    (white space)이라 하는데, 이러한 규칙을 '토큰 사이의 흰 공백은 무시한다.' 고 한다.

    실제로 흰 공백은 몇 개가 더 있다. 하지만, 사용자가 입력할 수 있는 흰 공백은 3개가

    전부이다.

    f(    x)

    {

        x*x+5

    }

  3단계. 함수가 리턴(return)하는 값이 뭔지를 명시(specify)한다.

  위와 같은 수학함수 f가 정의 되어있을 때 14라고 하는 것과 f(3)이라고 하
는 것은 표현만 다를 뿐 전부 수(number) 14를 나타낸다. 14 란 표현은 그렇
다 치고 f(3)은 왜 14인가?

  그것은 f(3)이 14란 값을 리턴하기 때문에 그렇다. 일반적으로 수학 함수는
값을 리턴한다. 하지만 C 함수 중에는 값을 리턴하지 않는 것도 있다. 그래서
함수가 값을 리턴한다면 '리턴한다' 라는 특별한 표현을 해 주어야 한다. 이것
은 특별하게 예약된 단어(reserved word) return을 사용해 나타낸다.

    f(    x)

    {

        t=x*x+5

        return t

    }

  새로운 t란 변수(variable)를 왜 도입했는지 알겠는가? return 문장 다음엔
함수가 리턴해야 할 값이 와야하는데, 계산 결과를 잠시 저장할 변수가 필요
하므로 t를 도입했다. 보통 return 문장은 함수의 마지막에 위치하지만, 항상
그런 것은 아니다.

  여기서 C의 중요한 규칙 한가지를 더 집고 넘어가자. 우리는 국어의 한 문
장(statement)이 끝났다는 것을 어떻게 아는가? '마침표를 보고'란 대답은 틀
린 대답이다. 우리는 우리가 배운 많은 경험과 단어 등을 통해 마침표가 없어
도 문장의 끝을 알 수 있다. 그렇다면 컴퓨터는 - 정확하게 말하면 컴파일러
(compiler)7)는 - 문장의 끝을 우리처럼 알 수 있을까? '모른다'. 그래서 우리
  7) 우리가 작성한 문장을 기계가 알 수 있는 형태로 번역하는 소프트웨어를 컴파일러라

    고 한다. 그러므로 C 컴파일러는, C언어를 이용하여 작성한 문장을 기계어(machine

    language)로 바꾼다.

는 "한국 언어(language)"가 아닌 "C 언어"를 통해 표현할 때 컴퓨터가 문장
의 끝을 알 수 있도록 특정한 표현을 해 주어야 한다8).

  8) 한국 언어 같은 언어를 자연어(natural language)라 하고, C언어 같은 언어를 형식

    언어(formal language)라 한다. 형식 언어는 정확한 문법에 의해 표현하여야 기계가

    인식할 수 있는 언어이다. 자연어에 의한 프로그래밍은 아직 연구 단계이다.

"C언어에서 문장의 끝은 세미콜론(semicolon; ;)으로 나타낸다."

  그러면 일단 문장이 뭔지 알아야 한다. return 도 문장이고, 수식(numerical
expression)도 문장이다. 뒤에 나올 변수의 형 선언(variable type declaration)
도 문장이다. 이 외에도 문장은 몇 가지가 더 있다. 문장의 수에 너무 당황할
필요는 없다. 문장은 몇 개의 단어와 간단한 규칙을 통해 만들어진다.

    f(    x)

    {

        t=x*x+5;

        return t;

    }

  4단계. 파라미터 변수와 함수 안에서 사용된 변수의 형을 선언한다.

  여러분은 1장의 '이진수'를 통해서 같은 숫자가 여러 비트를 사용하여 다양
하게 표현될 수 있다는 사실을 알고 있을 것이다. 함수 f가 2를 파라미터로
받았을 때 2를 메모리에 저장하기 위해 몇 비트를 사용할 것인가? 우리가 이
사실을 컴파일러에게 알려 주어야 한다. 실제로 이러한 '알려줌'은 비트의 수
(number)뿐만 아니라, '무슨 수를 어떻게 저장한다'라는 정보도 포함한다. C에
서 모든 변수는 사용하기 전에 알려주어야 하는데 이것을 변수의 형 선언
(type declaration)이라 한다. 그러므로 형 선언은 반드시 변수가 사용되기
전에 해 주어야 한다.

  그러므로 위의 경우 파라미터 x의, 그리고 임시 변수 t의 형 선언을 해야한
다.

    f(int x)

    {

        int t;

        t=x*x+5;

        return t;

    }

  int9)는 '변수가 메모리를 2바이트 차지하면서, 표현은 정수(integer)만이
  9) 정수(integer)의 약자이다.

허용된다'라는 의미이다. 독자들이 int 형으로 선언된 변수에 2바이트 이상의
정수나, 정수가 아닌 다른 형태의 수를 대입하려고 하면 컴파일러는 에러를
발생시킨다.

  파라미터의 형 선언과 임시변수의 형 선언이 조금 다르다. 일반적으로 변수
의 선언은 문장이기 때문에 ;을 포함한다. 하지만 파라미터 리스트는 문장이
아니므로 ;을 적어서는 안 된다.

  우리는 C언어의 중요한 규칙 한가지를 더 알게 되었다.

"변수는 사용하기 전에 반드시 선언해 주어야 한다."

  5단계. 함수의 리턴형을 선언한다.

  위의 함수 f가 다음과 같은 수식의 중간에 사용되었다고 하자.

    float10) i;

  10) float는 4바이트 실수(real)형을 의미한다.

    i=f(1)+1.0;

  위의 코드(Code)를 기계어로 바꾸기 위해 컴파일하는 과정을 살펴보자. 컴
파일러는 함수가 리턴하는 값을 잠시 저장하기 위해 메모리를 사용할 것이다.
그렇다면 f(1)이 리턴하는 값을 위해 어떤 형을 사용해야 할지 컴파일러가 어
떻게 알 수 있을 것인가? 이것 역시 우리가 미리 컴파일러에게 알려 주어야
한다. 이것은 변수 선언과 의미가 같다. 함수의 리턴 형은 함수 이름 앞에 명
시한다. 이것을 함수의 리턴형 선언(return type declaration)이라고 한다.

    int f(int x)

    {

        int t;

        t=x*x+5;

        return t11);

  11) 함수의 리턴 형이 int이므로 t가 int형 이라야 한다.

    }

  이제 모든 단계를 다 마쳤다. 이로써 수학 함수를 완전한 C함수로 바꿀 수
있게 되었다.

  아규먼트(argument)와 파라미터(parameter)

  아규먼트와 파라미터는 엄격한 의미에서 다르다고 한다(필자는 이 사실을
책이나 논문에서 확인한 바 없다). 아규먼트는 함수를 호출하는 쪽에서의 실
인자(actual parameter)를 가리키는 말이며, 파라미터는 함수를 정의하는 쪽
에서의 형식 인자(formal parameter)를 가리키는 말이다. 하지만, 이 책에서
는 아규먼트와 파라미터를 동일하게 취급할 것이며, 구분이 필요하다면 실 인
자(actual parameter) 혹은 형식 인자(formal parameter)라는 용어를 사용할
것이다.

...

void F(int k) {

    //어떤 일을 함

    //재미있는 일도 함.

    //k와 관계된 어떤 일도 함

}

...

void main() {

    int i=10;

    F(i);

}

..

  위의 예에서 main()의 F(i)에서 i의 값인 10을 - 의미가 명확하다면 i를 -
실 인자라고 한다(이 값이 실제로 전달된다). 함수를 정의하는 쪽에서의 k를
형식 인자라고 한다(실 인자를 받기 위해 형식적으로 적어둔 변수이다).

  관례(convention)

  규칙은 아니지만 '일반적으로 그렇게 하는 것'을 관례라고 한다.

  우리가 위에서 사용한 몇 개의 단어 중, int, return등은 언어에 의해서 미리
정의된 예약어(reserved word)이다. 이러한 예약어를 키워드(keyword)라고
도 한다. 하지만, 변수 이름 x, t나, 함수 이름 f등은 우리가 - 규칙에 맞다면
- 마음대로 정할 수 있다. 이러한 사용자에 의해서 정의되는 단어를 명칭
(identifier)이라 한다. 독자들은 몇 개의 예약어가 있는지, 명칭은 어떻게 정
하는지 궁금해 할 것이다. 하지만 걱정할 필요는 없다. 영어라는 언어는 수십
만 개의 단어가 있지만, C라는 언어에는 30여 개의 단어밖에 존재하지 않는
다!

  명칭은 마음대로 정해도 되므로12), 위의 함수 f는 아래와 같이 써도 같은

  12) 뒤에 정규 표현에서 명칭을 정의하는 법을 다룬다.

역할을 한다.

    int MyFirstFunction(int parameterX)

    {

        int temporary;

        temporary=parameterX*parameterX+5;

        return temporary;

    }

  함수 이름이나 변수 이름은 나중에 봐도 알기 쉽도록 하는 것이 좋다. 첫번
째 관례는 다음과 같다.

"함수의 이름과 변수의 이름을 정할 때 나중에 봐도 알기 쉽고 이해하기
쉽도록 이름을 정한다."

  또한 함수 이름은 대문자로 변수 이름은 소문자로 시작하도록 정한다. 변수
이름을 정할 때, 변수의 역할을 이해하기 쉽도록 접두어(prefix)를 붙이는데,
예를 들면,

nFileOpened

xCurrent

등이다. n은 수(number)를 의미하고, x는 좌표를 의미한다면, 사용자는 위의
문장을 보고, 각각이 파일의 열려진 개수와 현재의 x좌표를 나타내는 변수임
을 쉽게 알 수 있다13).

  13) 이러한 표기법을 처음 사용한 마이크로소프트의 프로그래머의 국적을 따서 헝가리

    표기법(hungarian notation)이라 한다.

  읽기 쉽게(readability) 하기 위해

 "변수의 선언 문장과 실행 문장 사이를 한 줄 띄운다"

    int f(int x)

    {

        int t;

        t=x*x+5;

        return t;

    }

  들여쓰기(indentation)를 하면 후에 설명할 제어문(control statement)등에
서 문장의 포함관계를 쉽게 알 수 있다.

"들여쓰기를 하라"

    int f(int x)

    {

        int t;

        t=x*x+5;

        return t;

    }

"한 문장은 되도록이면 한 줄에 적는다."

                        int f(int x){int t;t=x*x+5;return t;}

  즉 위와 같이 적어도 무방하다. 블락 구조를 나타내기 위해 쓰인 여는 브레
이스 { 가 하는 일 없이 한 줄을 차지하고 있는 게 보기 싫다면 다음과 같이
바꾸는 것이 좋다. 하지만 닫는 브레이스는 블락의 끝을 나타내기 위한 시각
적인 역할을 하므로 그대로 둔다.

    int f(int x) {

        int t;

        t=x*x+5;

        return t;

    }

  이젠 할 수 있다!

  이제 나머지 함수들을 C 함수로 바꿀 완벽한 준비가 되었다. 두번째 함수를
바꾸어 보자.

g(x,y)=f(x)+y

바꾼 결과는,

    int g(int x, int y) {

        int t;

        t=f(x)+y;

        return t;

    }

이다. 이렇게 함수의 몸체를 정의(define)할 때는 이미 정의된 다른 함수를 사
용 - 호출 - 할 수 있다. 위에서 f()가 정의되었으므로 위의 함수 g() 정의는
타당하다. 하지만 아직 정의되지 않은14) 함수 h()는 호출할 수 없다.

  14) g() 정의 후에 h()가 정의되었다고 해도, g() 입장에서는 h()가 정의된 것이 아니다.

    물리적인 순서 - 줄 번호 - 는 '정의되었음'의 판단 기준이다.

    int g(int x, int y) {

        int t;

        t=f(x)+y;

        return t;

    }

  파라미터가 2개 이상일 때는 콤마(comma; ,)로 구분을 한다. 이것을 파라미
터 리스트(parameter list)라 한다.

  함수의 헤더(Header)15) 부분을 int g(int x,y)라고 하면 안되는가?

  15) 함수의 정의에서 몸체를 제외한 부분을 머리 혹은 원형(prototype)이라 한다. 머리

    는 선언 지정자(declare specifier)와 선언자(declarator)로 나뉜다. 아래 그림을 참고

    하라.

      선언 지정자와 선언자로 구성되는 함수의 헤더

  안 된다. 이유는 안되도록 정했기 때문이다. 즉 C언어를 설계한 사람
들이 정한 문법에 의해 그러한 정의는 허락되지 않는다.

  다음은 가능한가?

    int x;

    x=3+5;

    int t;

    t=4*10;

  불가능하다. 선언문장은 반드시 처음에 모여 있어야 한다. 다음과 같이
고쳐야 한다. 하지만 C++에서는 위의 문장이 가능하다. 이러한 C++의 규칙을

"변수는 선언하고 싶을 때 선언한다."

고 한다.

    int x;

    int t;

    x=3+5;

    t=4*10;

  파라미터 선언과는 달리 임시 변수 선언은 형 이름이 한번만 와도 된다. 즉
위의 프로그램은 다음과 같이 표현해도 된다.

    int x,t;

    x=3+5;

    t=4*10;

3번째 함수를 바꾸어 보자.

y=3

바꾼 결과는

    int y(void) {

        return 3;

    }

이다. 이렇게 파라미터가 없다라는 표현을 void로 한다. C++에서는 파라미터
리스트가 void인 경우는 생략해도 된다16).

  16) 흔히 C에서도 이것을 생략해도 된다라고 알고 있는 독자들이 있다. C에서 생략하

    면 '모른다'는 의미이다.

4번을 바꾸어 보자. 4번 함수는 값을 리턴하지 않는다.

l(x,y)={원점에서 (x,y)까지 선을 그린다}

바꾼 결과는

    void l(int x,int y) {

        line(0,0,x,y)17);

  17) line()은 그래픽의 그리기 함수라고 가정한다. 이렇게 미리 만들어진 함수(built in

    function)중, 모든 컴파일러에서 지원하도록 규정된 함수들의 모임을 표준 함수

    (standard function)라고 한다. ANSI에서 정한 표준함수의 수는 100여 개 이다.

    }

이다. 이렇게 함수의 리턴값이 없다라는 표현도 void로 한다. 주의 사항은 파
라미터 리스트를 생략한 경우는 void이지만, 함수의 리턴 형을 생략한 경우는
int이므로 반드시 void를 명시해야 한다.

  자 이제 우리는 어떤 수학 함수라도 C의 함수로 바꿀 수 있을 것 같다. 마
지막으로 억수로 중요한 한 가지 규칙을 더 언급하자. 위와 같이 4개의 함수
가 있을 때 시작하는 함수는 어느 것으로 할 것인가? 여기에 관한 규칙은 다
음과 같다.

"시작하는 함수는 main()이다. 반드시 그리고 유일하게 1개 있어야 한다."

  시작하는 함수는 소문자 main()이다18). 실행프로그램이 운영체제에 의해서
  18) C언어 처럼 대소문자를 구분하는 언어를 대소문자에 민감한 언어(case sensitive

    language)라 한다.

로드(load)된 후 운영체제는 제일 먼저 main을 호출한다. 그러므로 실행 파일
을 만드는 소스마다 반드시 1개만의 main() 함수를 가져야 한다. main()의 원
형은 다음과 같다.

void main(void)

  함수도 선언해야 한다.

  아래의 프로그램은 무엇이 잘못인가?

    void main() {

        int i;

        i=f(3);

    }

    int f(int x) {

        int t;

        t=x*x+5;

        return t;

    }

  함수 main()에서 f()를 호출하고 있다. 하지만 컴파일러가 i=f(3) 이란 문장
을 기계어로 번역하기 위해서는 먼저 함수 f()가 제대로 사용되었는지에 대한
정보가 필요하다. 함수 f()가 정수 2개를 파라미터로 받는다면 어떻게 에러를
발견할 수 있는가? 만약,

i=f(3,4)

라고 사용했다면, 컴파일러는 에러를 출력할 수 있어야 한다.

  컴파일러는 함수 호출이 타당한지 검사하기 위하여 함수의 머리 부분을 특
정한 테이블 - 심벌 테이블(symbol table) - 에 저장하여 둔다. 함수 호출
문장을 만나면 컴파일러는 자신이 현재 유지하고 있는 테이블에서 함수를 찾
아서 다음과 같은 사항을 검사한다.

  (1) 리턴 형이 맞는가

  (2) 파라미터의 개수는 몇 개이며, 형은 맞는가?

  이러한 검사가 실패하면 컴파일러는 에러 메시지를 출력한다. 일반적으로
컴파일러는 코드를 생성하기 위해 소스를 두 번 스캔(scan)한다19). 첫 번째

  19) 이러한 컴파일러를 2번 검사 컴파일러(2 pass compiler)라 한다.

스캔에서 함수와 변수에 관한 정보를 심벌테이블에 저장하며, 함수의 주소를
결정하기 위하여 함수의 사용이 맞게 되었는지 등도 검사한다. 두 번째 스캔
에서 실제로 코드를 생성한다.

  하지만 main()에서 함수 호출 f()를 만난 시점에서 컴파일러는 f()에 관한
어떤 정보도 유지하고 있지 않다. 그러므로 함수의 사용이 타당한지를 검사할
수 없다. 그러므로 컴파일러는 에러 메시지를 출력한다. 그러므로 변수를 쓰기
전에 선언해야 하듯이 함수도 쓰기 전에 선언해야 한다. 규칙은 다음과 같다.

"함수는 호출 전에 반드시 선언되어야 한다."

  소스는 다음과 같이 수정되어야 한다.

    int f(int x);

    void main() {

        int i;

        i=f(3);

    }

    int f(int x) {

        int t;

        t=x*x+5;

        return t;

    }

  함수의 선언은 함수의 머리 부분만으로도 충분하다. 그리고 문장이므로 끝
에 ; 을 명기한다. main() 밑은 실제의 함수 f()를 정의하고 있으므로, 함수
정의(definition)라고 한다.

  여러분은 위의 스타일이 보기 싫으면 다음과 같이 소스를 수정할 수 있다.

    int f(int x) {

        int t;

        t=x*x+5;

        return t;

    }

    void main() {

        int i;

        i=f(3);

    }

  위의 스타일에서는 함수 f()에 관한 선언(declaration)은 없어도 된다20). 위
  20) 있어도 된다는 의미를 포함한다.

와 같이 함수가 정의되었을 때, '함수의 선언과 정의를 동시에 했다.'고 한
다. 필자는 위와 같은 스타일(style)을 선호한다. 하지만 위와 같은 스타일이
싫은 사람은 선언과 정의를 따로 하는 것을 즐겨하기도 한다21). 비록 위의 프
  21) 반드시 선언을 필요로 하는 함수가 있으므로, 선언의 역할을 명확히 알고 있어야

    한다.

로그램은 아무 것도 출력하지 않지만 코드를 입력해서 컴파일 해 보라. 훌륭
히 컴파일 될 것이다.

  컴파일러는 ANSI에서 정한 표준 함수(standard function)를 미리 만들어서
제공하는데, 이를 표준 함수라고 한다22). 표준 함수는 컴파일러나 운영 체제
  22) 성급하게 어떤 종류의 표준함수가 몇 개나 있는지 알려고 하지 말라. C의 문법에

    익숙해진 독자들은 그 어려운 작업(?)을 하여도 좋다.

에 상관없이 동일한 기능을 제공하는데, 운영 체제의 특성 때문에 오히려 어
려운 상황이 발생하기도 한다. 우리가 제일 먼저 사용하게 될 표준 함수는

printf()

이다. 이 함수는 표준 출력(standard output)이라고 불리는 파일(file)에 문
자열(string)을 - 아직 표준 출력, 파일, 문자열을 정의하진 않았지만 - 출력
한다.

  예를 들면

printf("I love the God");

는 화면(screen)의 현재 커서(cursor) 위치에 문자열 "I love the God"를 출력
한다. 이 프로그램을 완벽하게 C로 만들어 보자.

    void main() {

        printf("I love the God");

    }

  위의 프로그램은 아직 부족하다. '함수는 쓰기 전에 선언해야 한다.'라는 중
요한 규칙을 만족하지 못하고 있다. printf()의 선언문이 필요한 것이다.

  여기서 우리가 직면한 문제는 '수백 개나 되는 표준 함수의 선언을 어떻게
일일이 쓸 때마다 선언을 적어 줄 것인가?' 이다. 그래서 C언어는 이러한 문
제를 해결하기 위한 훌륭한 해결책을 제시해 놓았다. 선언을 미리 해서 한 개
의 파일에 모아 놓은 다음, 이 파일을 끼워 넣기(include) 하는 것이다.

  먼저, 표준 함수를 종류와 기능 별로 구분한 다음, 구분된 각각의 부류의 함
수들에 대해 함수 선언을 포함한 파일을 만든다. 이러한 파일은 대개 함수의
머리 부분(header)만을 포함하므로 헤더 파일(header file)이라고 하며, 파일
의 확장자로 .H를 - *.h - 붙인다. 다음과 같은 표준 헤더 파일이 있다.

  stdio.h : 표준 입출력에 관한 함수의 선언이 들어 있다.

  stdlib.h : 표준 함수 중 입출력 외의 함수들이 들어 있다.

  math.h : 표준 수학 함수의 선언이 들어 있다.

  디스크에 별도로 존재하는 이러한 외부 파일을 끼워 넣는 명령문은

#include

이다. 예를 들면 printf()는 표준 출력에 사용되는 함수이므로, stdio.h에 선언
이 들어 있다23). 그러므로 완벽한 소스는 다음과 같다.

  23) 일반적으로 컴파일러는 함수가 어느 헤더 파일에 포함되어 있는지에 대한 정보를

    확인하는 방법을 제공한다. 여러분이 사용하는 컴파일러에 따라 F1 혹은 ^F1을 누르면

    된다.

    #include <stdio.h>

    void main() {

        printf("I love the God");

    }

즉 헤더 파일의 이름을 작다(less than; <)와 크다(greater than; >) 기호 사
이에 적어준다. 어떤 독자들은 왜

#include "stdio.h"

라고 사용하지 않는지에 대해 의문이 생길지도 모른다. 후에 이러한 것을 살
펴 볼 것이다.

  독자들이 다음으로 주목해야 하는 사실은 include 앞에 붙은 특수 심벌 #이
다. 왜 그냥

include <stdio.h>

가 아니고,

#include <stdio.h>

인가? 독자들은 #include의 처리 과정에 주목해야 한다. #include문은 컴파일
러가 코드를 생성하기 전 단계에 처리된다. 즉 디스크에 존재하는 stdio.h 파
일을 그 자리에 끼워 넣은 다음, 끼워 넣어져서 확장된 소스를 컴파일러가 컴
파일하는 것이다. 독자들은 메뉴에서 컴파일을 선택해서 컴파일 과정을 살펴
보라. 독자들이 작성한 소스에 비해 훨씬 긴 줄을 컴파일한 것을 살펴 볼 수
있다. #include 문에 의한 소스 확장(extension)의 결과이다.

  이렇게 컴파일 전에 처리되는 명령문을 전처리(preprocessing24)) 명령문,
  24) 컴파일 과정을 processing이라고 보고, processing 전에(pre) 처리된다는 의미이다.

혹은 컴파일러 지시자(compiler directive)라고 한다. C에서 전처리 명령문
은 모두 특수 문자 # 으로 시작한다. 예를 들면 #define, #ifdef, #endif 등이
있다.

  이제 여러분은 어떠한 수학 함수가 주어지더라도 이것을 C함수로 만들 수
있는 능력을 가지게 되었다. 비록 시작이기는 하지만.

80/80 규칙(80/80 rule)

  이 책의 내용을 전개해 가는데 필자는 되도록, 80/80 규칙을 따를 것이다.
이 규칙은 '80%의 사람이 투자해야 할 프로그램의 80%'를 의미한다. C++
파트를 전개하는 데에는 프로그램의 장식에는 전혀 신경 쓰지 않을 것이며,
문제 파트를 전개하는 데에는 특별한 몇 경우를 제외하고는 역시 80/80 규칙
을 따를 것이다.

  소스 문자 집합(source character set)

  C언어는 어떤 문자들로 구성할 수 있을까? C/C++언어의 토큰을 구성하는
문자는 다음과 같다.

a b c d e f g h i j k l m n o p q r s t u v w x y z

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

0 1 2 3 4 5 6 7 8 9

_ { } [ ] # ( ) < > % : ; . ? * + - / ^ & | ~ ! = , \ " '

  위의 문자들은 변수(variable), 명칭(identifier), 숫자(number), 기호(symbol)
등을 형성한다. 이러한 것들을 모두 토큰(token)이라 한다.

  영문자와 숫자를 제외한 특수문자의 발음은 아래와 같다.

_	underscore(밑줄)

{	open brace or open curly bracket(여는 대괄호)

}	close brace(닫는 대괄호)

[	open bracket(여는 대대괄호)

]	close bracket(닫는 대대괄호)

#	pound(sharp or number)

(	open parenthesis(여는 괄호)

)	close parenthesis(닫는 괄호)

<	less than(보다 작다)

>	greater than(보다 크다)

%	percent

:	colon

;	semicolon

.	period(점)

?	question mark(물음표)

*	asterisk or star(별표)

+	plus

-	minus

/	divide

^	caret

&	ampersand

|	vertical bar(수직 바)

~	tilde(틸데: 발음에 주의하라)

!	exclamation mark(느낌표)

=	equal

,	comma

\	back slash(역 스래쉬)

"	double quotation mark(이중 인용 부호)

'	single quotation mark(인용 부호)

  아래표는 ASCII 문자 집합의 발음을 모두 표시한 것이다.

             ASCII 문자 집합

세글자 한소리 절차(trigraph sequence)

  ??로 시작하는 3중음자가 자판의 몇 심벌을 나타낸다. 표준안에 언급은 없
지만, 우리가 일반적으로 사용하는 #,[ 같은 것이 특정 국가의 자판(keyboard)
에 없거나, 이러한 심벌이 특정한 나라에서는 종교적인 심벌로 쓰이는 것 같
아, 이러한 특별한 3중음자를 추가한 것 같다. ?는 3항 연산자(?:연산자)가 있
으므로, 특별히 두개의 연속한 물음표(??)를 사용한다.

  지원하는 3중음자는 다음 표와 같은 9개이다.

trigraph  replacement

trigraph  replacement

trigraph  replacement

??=		#

??(		[

??<		{

??/		\

??)		]

??>		}

??'		^

??!		|

??-		~

             ISO표준 3중음자

  아래의 소스를 참고하라.

#include <stdio.h>

??=define arraycheck(a,b) a??(b??) ??!??! b??(a??)

//#define arraycheck(a,b) a[b] || b[a]

void main() {

    int a[3]={1,3,5};

    printf("%d\n",arraycheck(a,2));

}

결과는

1

이다.

  실습문제

1.


8. 이진수

8. 만물은 음양의 이진법
으로 나타낼 수 있다.

  옛날 중국과 우리 나라에서는 모든 것을 음양으로 나누어서 따지는 경향이
강했으며, 지금도 그 전통이 뿌리 깊게 살아 있다. 우리 나라의 태극기가 이것
을 잘 상징하고 있다.

<그림 ?> 팔괘(八卦)

  옛날에는 두 가지 막대를 써서 앞으로 일어날 좋고 나쁜 일을 점쳤는데, 이
것을 역(易)이라고 부른다. 우리 나라 태극기의 네 구석에 있는 것은, 이 역의
원리의 일부이다. 팔괘라고 부르는 이 원리는 위의 8가지인데, 지금 "－"(양)
을 1, "--"(음)을 0으로 생각하고 고쳐 쓰면,

111, 011, 101, 001, 110, 010, 100, 000

과 같이 되어, 2진법의 0부터 7까지의 수와 꼭 들어맞는다. 이 사실을 처음으
로 지적한 사람은 독일의 철학자 라이프니쯔(Leibniz, 1646-1716)였다.

  음양 사상이란 태양과 달, 남자와 여자, 홀수와 짝수 등과 같이 세상의 모든
것을 음과 양으로 분류해서 생각하는 태도이다.

  이 음양 사상이 유럽으로 전해졌으며, 위대한 철학자와 과학자 중에는 그
영향을 받은 사람이 적지 않았다. 그 대표적인 예가 라이프니쯔에 의해 발명
된 2진법이다. 지금의 컴퓨터의 논리적 구조는 2진법인데, 이 2진법이 수학이
사실은 동양의 음양 사상의 영향을 받아 태어났다는 사실은 아주 흥미를 끈
다.

  지금 이 생각을 정수에 국한시켜 생각하면 모든 정수는 짝수와 홀수로 나누
어 생각할 수 있다. 짝수의 대표로 0, 홀수의 대표로 1을 뽑아 본다. 이 두 수
사이의 덧셈과 곱셈을 생각하면 다음 표와 같이 나타난다.

+

0

1

×

0

1

0

0

1

0

0

0

1

1

2

1

0

1

  위의 표를 보면 1+1=2인 경우만을 제외하고 계산 결과는 모두 0 아니면 1
이다.

  2는 짝수이기 때문에 이것을 짝수의 대표 0으로 바꾸어 놓아 보자. 즉,

1+1=0

이라고 해 보자. 그러면 이 덧셈표는 아래 표와 같이 된다.

+

0

1

0

0

1

1

1

0

  이것은 아래의 표에서 짝수, 홀수를 0, 1로 바꾸어 놓은 것이다.

+

짝수

홀수

×

짝수

홀수

짝수

짝수

홀수

짝수

짝수

짝수

홀수

홀수

짝수

홀수

짝수

홀수

  이렇게 함으로써 두 원소 0과 1로 된 집합

{0, 1}

에서 덧셈과 곱셈이 정해지고 계산의 결과도 이 집합의 테두리 안에서 얻어지
게 된다.

  정수 전체로 된 집합을 Z로 나타내 보자. 즉,

Z={..., -4, -3, -2, -1, 0, 1, ...}

이에 대해서 위의 두 원소로 된 집합을 Z2로 나타내 보자. 즉,

Z2={0, 1}

  Z2라는 집합은 Z의 원소를 짝수, 홀수라는 성질에 의하여 두 종류로 나누어
이 종류에 대해서만 생각하여 얻은 작은 집합이다.

  이 세상 모든 것에 수를 대응시키고 이것을 짝수, 홀수로 분류하여 0과 1만
으로 대표시킨 생각은 이 세상 모든 것을 음과 양으로 분류시킨 것과 근본적
으로 같은 생각이다. 그러나 동양에서는 끝까지 논리적으로 추구하지 않고 이
것을 점이나 미신으로 만들어 버렸다. 한편 최근의 컴퓨터는 True에는 1,
False에는 0을 대응시키고 위해서 만든 표로써 전자 회로를 설계하기도 한다.
모든 명제는 0과 1로 된다는 사상이다. 이처럼 같은 발상이 과학이 될 수도
있고 미신으로 그칠 수도 있다.

보충해 주는 수: 보수(Complement Number)

  r진수 m자리수 n의 보수는

rm-n

이다. 10진수에 대해서 생각해 보면, 7의 보수는 101-7=3이다. 77의 보수는
102-77=33이다. 10이나 100을 만들기 위해 보충해야 하는 수가 각각 3과 33이
라는 의미이다.

  보수는 뺄셈을 위해 사용할 수 있다. 예를 들어 10진수 8-3을 계산한다고
하면, 8+(3의 보수)=8+7=15 인데 1자리의 뺄셈이므로 십의 자리 1을 버리면,
8+7=5가 되어 원하는 결과가 얻어진다. 보수의 이러한 성질 때문에 컴퓨터는
정수(Integer)를 나타내기 위해 보수를 사용한다.

  2진수에서 1의 보수(1's Complement)와 2의 보수(2's Complement)를
구하는 것은 쉽다. 1의 보수는 단순히 1과 0을 토글(Toggle)시킴으로 구할 수
있고, 2의 보수는 1의 보수를 구한 다음, 결과에 1을 더하면 구할 수 있다. 예
를 들어, 1001011100의 1의 보수는 0110100011이며, 2의 보수는 0110100011 +
1 = 0110100100이다.

  2의 보수는 더 간단하게 구하는 방법이 있는데, 오른쪽에서 왼쪽으로 처음
으로 1을 만날 때까지 스캔(Scan)하며 만난 숫자를 적다가, 처음 1 이후의 모
든 비트를 토글하면 된다.

<그림 ?> 2의 보수를 구하는 법

  컴퓨터가 정수를 나타내는 방식은 일반적인 2진수 체계를 따르지만, 음수를
나타내기 위해서 2의 보수를 사용한다. 2진수 n비트로 나타낼 수 있는 수의
범위는

0∼2n-1

이다. 그러므로 unsigned char형은 0∼28-1, 즉 0∼255를 나타낼 수 있고,
unsigned short int형은 2바이트이므로, 0∼216-1, 즉 0∼65535를 나타낼 수 있
다. 음수인 경우는 어떻게 할까? 음수인 경우는 가장 중요한 비트(Most
Significant Bit: MSB)를 부호 비트(Sign Bit)로 사용하여, MSB가 1인 경우
는 이 수의 2의 보수를 취한 값을 음수로 한다. 즉 부호는 음수로 결정된 상
태에서 이 수의 2의 보수를 구하여 값을 취하므로, MSB자체도 값의 결정에
사용될 수 있다. 부호 있는 3비트로 1의 보수와 2의 보수로 각각의 정수를 표
현해 보면 <표 ?>와 같다.

1의 보수

정수값

2의 보수

정수값

000

001

010

011

100

101

110

111

+0

+1

+2

+3

-3

-2

-1

-0

000

001

010

011

100

101

110

111

+0

+1

+2

+3

-1

-3

-2

-1

<표 ?> 1의 보수와 2의 보수의 부호 있는 정수값

  예를 들어, 101의 경우 1의 보수에 대해서,

MSB가 1이므로 음수

101의 1의 보수는 010=2

이므로 -2가 된다. 하지만 2의 보수에 대해서는,

MSB가 1이므로 음수

101의 2의 보수는 011=3

이므로 -3이 된다. 이 세상에 존재하는 모든 디지털 컴퓨터가 2의 보수를 이
용하여 정수를 표현하는 이유는 올림수를 무시해도 되는 이점 때문이다. 예를
들어 3-2를 하는 경우 1의 보수인 경우,

011+101=1000

밑줄 친 것처럼 올림수가 발생한 경우 이를 다시 더해야 한다. 그러므로,

000+1=001

이 되어 원하는 결과 1이 얻어진다. 하지만 2의 보수인 경우는 단순히 올림수
를 무시하면 되므로 +연산을 적게 사용하므로 훨씬 효율적이다.

<그림 ?> 2의 보수에서 올림수의 무시

  2의 보수로 수를 표현했을 때, n비트의 부호있는 정수의 범위는 다음과 같
다.

-2n-1∼2n-1-1

그러므로, signed short int인 경우 -32768(-215)∼32767(215-1) 범위의 수를 표
현할 수 있다. 그렇다면, 아래 프로그램의 결과는 얼마가 인쇄될까?

#include <stdio.h>

void main() {

    char c=129;

    printf("%d\n", c);

}

  129는 2의 보수로 0000 0000 1000 0001 이다. 이 숫자를 8비트 char변수 c
에 대입하는 과정에서 상위 8비트는 잘려진다. 그러므로 c는 1000 0001이다.
이것을 printf가 출력하는데 부호 있는 정수형이므로 2의 보수 체계를 따라

MSB가 1이므로 음수

1000 0001의 2의 보수는 0111 1111=127

이므로 -127이 인쇄된다.

비트 플래그(Bit Flag), 비트 마스크(Bit Mask)

  위에서 설명한 2진수의 개념이 도대체 나와 무슨 상관인가? 하지만 여러분
은 곧 위의 2진수 개념이 여러분과 무진장 많이 상관되어 있다는 것을 알게
될 것이다.

  아래의 프로그램은 구구단을 출력하는 프로그램인데 구구단을 출력하다가
사용자가 임의의 키를 누르면 종료하도록 만들려고 한다. 아래에 설명 부분을
보지 말고 여러분이 직접 고쳐보기 바란다.

#include <stdio.h>

#include <conio.h>

void main() {

    int i=1,j;

    clrscr();

    while (i<100) {

        ++i;

        j=1;

        while (j<100) {

            printf("%5d*%5d=%8d",i,j,i*j);

            ++j;

        }//while

    }//while

}

  문제는 while문안에서 break를 사용해야 한다는 것인데, 단순하게 안의
while문안에서

if (kbhit()) break;

해서는 문제 해결이 되지 않는다. break는 자기를 둘러싼 가장 가까운 반복
구조의 블락을 탈출하기 때문이다. 즉 break는 안의 while문을 탈출하기만
할뿐이다. 그러므로 안의 while문에서 어떤 사건이 발생했다는 것을 기억시키
기 위해서 특정한 변수를 사용하여야 한다. 변수가 이러한 목적으로 사용되었
을 때 깃발(Flag) 변수라고 한다. 깃발 변수를 사용한 소스를 아래에 리스
트하였다.

#include <stdio.h>

#include <conio.h>

void main() {

    int i=1,j;

    int flag;//사건을 기록하기 위해서 사용된다

    clrscr();

    while (i<100) {

        ++i;

        j=1;

        while (j<100) {

            printf("%5d*%5d=%8d",i,j,i*j);

            ++j;

            if (kbhit()) {

                flag=1;

                break;

            }//if

        }//while

        if (flag==1) break;//사건이 일어난 경우 while을 탈출한다

    }//while

}

  어떤 정수형 변수의 이진 비트열의 각각의 비트들이 이러한 깃발 변수로 사
용되었을 때를 비트 플래그라고 한다.

  예를 들어 정수형 변수는 16비트이므로 16가지의 On/Off 상태를 기록할 수
있다. 만약 상태의 개수가 4가지라면, 상태를 나타내는데 2비트가 필요하므로
8가지의 깃발 변수로 사용할 수 있다.

  예를 들어 컴퓨터 바둑 프로그램을 구현할 때 판(Board)의 각각의 위치에
대해,

  (1) 판의 현재 상태: 2비트(비었음, 검은돌, 흰돌, 가장자리)

  (2) 이웃한 검은색 돌의 수: 2비트(Neumann이웃이므로 최대 4까지 가능)

  (3) 이웃한 흰색 돌의 수: 2비트

  (4) 덩어리 번호: 8비트

  (5) 마킹을 위한 임시 비트: 2비트

를 유지한다고 하자. 그러면 여러분은 정수형 배열 board[19+2][19+2]를
선언해서 각각의 판 위치에 대해 <그림 ?>처럼 정보를 유지할 것이다.

<그림 ?> 판이 유지하는 정보

  얼마만큼의 공간이 효율적인가? 위처럼 사용하면 board가 차지하는 메모리
는 21×21=441 바이트이다. 만약 여러분이 비트 플래그를 몰라서 구조체 배열
을 사용한다면,

    struct BOARD {

        char nState, nBlack, nWhite;

        unsigned char nGroup;

        char nMarking;

    } board[21][21];

처럼 선언해서 각각의 자리마다, 5바이트를 사용하므로, 21×21×5=2205 바
이트를 사용해야 한다. 또한 공간만이 문제가 아니라 속도나 함수의 파라미터
수 등이 문제가 될 수 있다.

  다음으로 우리가 고려해야 할 문제는 '어떻게 각각의 비트 플래그를 갱신
하는가?' 이다.

<그림 ?> board[y][x]의 상태

  예를 들어 <그림 ?>와 같은 상태의 board[y][x]의 값은,

00 00000100 10 01 01

이다. 이는 (x,y)에 검은 돌이 있으며, 4-연결 이웃에 각각 검은돌, 흰돌이 1
개, 2개 있음을 나타내면, 덩어리 번호는 4임을 나타낸다. 이 상황에서 4의 마
지막 남은 이웃 자리에 검은 돌을 놓았다고 하자. 그러면,

00 00000100 10 10 01

이 되어야 한다. 어떻게 밑줄 친 비트의 일부만을 갱신할 것인가? 절차는 다
음과 같다.

  (1) 갱신하고자 하는 비트열만을 일반 정수 형태로 변환한다.

  (2) 비트열을 갱신한다.

  (3) 갱신된 비트열을 원래 자리에 다시 가져다 놓는다.

이를 갱신하는 루틴은 아래와 같이 쓸 수 있다.

  unsigned int i=board[y][x];//i= 00 00000100 10 10 01

  unsigned int t;

  t=(i & 0x000c)>>2;//0x000c는 이진수로 0000 0000 0000 1100이다

  ++t;//t가 1에서 2가 된다

  i=(i & 0xfff3) | (t<<2);//0xfff3은 이진수로 1111 1111 1111 0011이다

  board[y][x]=i;

  위의 코드를 처음 보는 사람은 이해가 되지 않을 것이다. 하지만 위의 코드
는 비트 마스크의 조합에 불과하다. 비트 마스크는 비트 연산자 &, | 를 사용
하여 특정한 비트를 1로 혹은 0으로 만드는 것을 말한다. & 와 | 연산은 아래
와 같은 특징을 가진다.

<그림 ?> 비트 마스크

  즉, 1과 & 하면 원래의 비트가 유지되며, 0과 & 하면 모두가 지워진다. 또
한 1과 | 하면 모두가 1이 되며, 0과 | 하면 원래의 비트가 유지된다. 이러한
원리를 이해하면 위의 소스 코드가 이해되리라 믿는다.

실습문제

1. 아래의 출력 결과는 얼마인가?

    #include <stdio.h>

    void main() {

        int i=7;

        printf("%d\n",~i+1);

    }

2. 아래의 출력 결과는 얼마인가?

    #include <stdio.h>

    void main() {

        int i=0x5555, j=0x1234, k;

        k=i^j;

        printf("%x\n",k);

        printf("%x\n",k^j);

    }

3. 아래의 프로그램에서 Product(a,b) 함수는 a*b를 쉬프트(shift) 연산과 모듈
로(modulo) 연산만을 사용하여 계산한다. Product() 함수를 자세히 설명하라
(힌트: Booth 알고리즘).

#include <stdio.h>

long Product(long a,long b) {

    long p=0;

    while (a>0) {

        if (a%2==1) p+=b;

        a>>=1;

        b<<=1;

    }//while

    return p;

}//Product()

void main() {

    printf("%ld\n",Product(3,7));//결과는 3*7=21이다.

}

4.


4. 2진수, 2의 보수, 부동 소수점 표기

4. 변수는 쓰기 전에 선언
해야 한다. 데이터 형
(data type)

#include <stdio.h>

void main() {

    char c=129;

    printf("%d\n",c);

}

  위 프로그램의 실행결과는 얼마일까? 다음과 같은 대답을 한 사람은 반드
시 이 장을 읽어서 완벽하게 이해해야 한다.

  (1) 129

  (2) 얼마가 찍힐 지 모른다.

  (3) char형은 -128∼+127까지만 표현할 수 있는데, 129를 대입했으므로, 오
버플로우(overflow) 에러이다. 결과는 알 수 없다.

  위의 대답은 모두 틀렸다. 정답은 다음과 같다.

"char는 1바이트 정수형인데, 앞에 선언 변경자 unsigned 가 없으므로, 부호
있는 1바이트 정수형이다. 모든 언어는 정수를 나타내기 위해 2의 보수를 사
용하는데, 129의 부호 있는 2의 보수 표현은 -127이므로, -127이 찍힌다."

  2진수의 자세한 표현이, 얼마나 많이 중요하게 쓰이는지 독자들은 곧 알게
될 것이다. 자료형의 내부 표현을 아는 것은 아무리 강조해도 지나치지 않다.
이 세상의 모든 컴퓨터는 정수를 표현하기 위해 2의 보수를 사용한다. 일반적
으로 정수를 표현하는 방법은 부호와 가중치(sign & magnitude)에 의한 방법,
1의 보수(1's complement), 2의 보수(2's complement) 방법 등이 있다. 각 표
현의 특징을 살펴보자.

  부호와 가중치에 의한 방법은 가장 왼쪽 비트(MSB; most significant bit1))

  1) 가장 왼쪽 비트는 가중치가 가장 크므로, MSB라고 한다.

를 부호 비트(sign bit)로 사용하며, 나머지 비트들을 가중치(magnitude)로 사
용한다. 3비트 표현을 써 보면 아래와 같다.

표현

값

000

001

010

011

100

101

110

111

0

1

2

3

-0

-1

-2

-3

  표 4-1	부호와 가중치에 의한 부호 있는 정수 표현(3비트의 예)

  만약 음수를 표현하지 않는다면, 0∼7까지의 수를 표현 가능하다. 이것은 다
음과 같은 공식으로 표현된다.

0∼2n-1

  위의 경우는 3비트를 사용하므로, 0∼23-1의 범위가 된다. 110을 -2로 결정
하는 것은 두 단계를 거친다.

  (1) 부호 결정: MSB가 1이므로, 음수이다.

  (2) 값 결정: MSB를 제외한 나머지 비트들의 가중치를 결정한다. 10이므로
2이다.

  이와 같이 부호와 가중치에 의한 방법에서는 MSB는 부호만 결정할 뿐 값
에는 영향을 미치지 않는다. 하지만 이 방법은 어떠한 컴퓨터도 정수를 나타
내기 위해 사용하지 않는다. 다음에 설명할 보수를 사용하여 정수를 표현한다.

  1의 보수와 2의 보수 각각에 대해 값을 결정한 표를 아래에 나타내었다.

표현

값

000

001

010

011

100

101

110

111

0

1

2

3

-3

-2

-1

-0

표현

값

000

001

010

011

100

101

110

111

0

1

2

3

-4

-3

-2

-1

  표 4-2	1의 보수, 2의 보수에 의한 부호 있는 정수 표현

  보수에 관한 사항은 "8. 2진수, 2의 보수, 비트 플래그"를 참조하기 바란
다. 100의 보수에 의한 값은 다음 두 단계를 거쳐 계산한다.

  (1) 부호 결정: MSB가 1이므로 음수이다.

  (2) 값 결정: MSB를 포함한 전체 비트의 보수가 값이다. 100의 1의 보수는
011이므로 3이다. 100의 2의 보수는 100이므로 4이다.

  그러므로 100은 1의 보수에 의해 -3, 2의 보수에 의해 -4이다. 부호와 가중
치에 의한 표현법과는 달리, 부호 결정 이후에 전체 비트가 값을 결정하기 위
해 사용되었다. "8. 2진수, 2의 보수, 비트 플래그"에서 설명한 이점 때문에
컴퓨터는 2의 보수를 사용하여 정수 - 양수, 음수 모두 - 를 표현한다. 그렇
다면, 2의 보수 표현이 음수를 표현하는지, 양수만 표현하는지 어떻게 알 것인
가? 이러한 사항을 문법이 지원해 주어야 한다. 여러분이 1바이트의 부호 없
는 정수형 변수2)를 선언하고 싶으면 다음과 같이 한다.

  2) 일반적으로 char는 아스키 1 문자(character)를 나타낼 수 있기 때문에, 문자형

    (character type)이라고 번역하기도 한다.

unsigned char i;

  부호 있는 정수형은 다음과 같이 선언한다.

signed char i;

  2바이트, 4바이트 정수는 char 대신에 각각 short, long을 사용한다. 그러므
로 4바이트 부호 있는 정수는

signed long i;

라고 선언한다. 일반적으로 부호의 유무를 나타내는 signed 나 unsigned는 생
략하면 signed가 되므로, 생략하여

long i;

라고 한다. short나 long다음에는 이것이 정수(integer)임을 확실히 하기 위하
여 int가 와도 된다. 그러므로

long int i;

라고 해도 같은 표현이다. 또한 정수는 일반적으로 2바이트이므로 - 항상 그
런 것은 아니다 - short int i;를 int i;라고 사용하기도 한다. 이제까지 설명한
변수의 형 선언 앞에 붙을 수 있는 변경자(modifier)를 포함한 정수 선언의 문
법은 다음과 같다. 밑줄 친 부분은 생략하면 그 값이 디폴트(default)임을 의
미한다.

[signed|unsigned] [short|long] int <variable>;

  초기화(initialization)

  변수가 선언되고 초기화되어야 한다면, 선언하면서 초기화하는 것이 간단하
고 읽기 좋다. 아래의 예는 정수형 변수 i를 선언한 뒤 i를 1로 초기화하고 있
다.

    int i;

    i=1;

  위의 코드는 다음과 같이 간단하게 적을 수 있다.

    int i=1;

  일반적인 것은 아니지만■, 초기값은 브레이스({})로 묶어도 된다.

             이러한 블락 구조의 초기화 - {}를 사용하는 초기화 - 는 배열, 클래스,
          구조체 등 복합형(compound type)의 변수 혹은 객체를 초기화하기 위해 사
          용한다. 이러한 초기화에서 사용하는 {}는 엄격한 의미에서 블락이 아니다.

    int i={1};

  아래의 예를 참고하라.

#include <stdio.h>

void main() {

    int i;

    int j=2;

    float k={1.2};

    i=1;

    printf("%d,%d,%f\n",i,j,k);

}

  결과는

    1,2,1.200000

이다■.

             1.2가 1.200000로 인쇄된 것은 밑의 진보된 주제에서 설명한다.

  정수(integer)같은 기본형의 초기화 외에 배열, 구조체, 클래스의 초기화는
각각의 주제가 나올 때마다 다룬다.

함수형 초기화(functional initialization)

  C++의 클래스(class)가 소개된 이후 기본형의 함수형 초기화가 가능해졌다.
이것은 기본형의 생성자(constructor) - 존재하는 것은 아니지만 - 를 호출하
는 것 같으므로, 자연스럽다. 예를 들면

int i=1;

은

int i(1);

과 같다. 비록 2번째 예의 i선언이 함수 i를 호출하는 것 같지만, 이것은 정수
변수 i를 1로 초기화하는 문장이다. 함수 호출처럼 보이므로, 함수형 초기화라
한다.

  기본형의 함수형 초기화는 중요한 것은 아니지만, 반드시 함수형 초기화를
해야하는 곳이 있으므로, 함수형 초기화는 C++에서 중요하다. 이것은 '함수형
초기화'에서 다룬다.

  실수(real number)는 어떻게?

  독자들은 2의 보수를 이용하여 정수를 표현함을 알았다. 하지만, 실수(real
number)는 어떻게 표현할까? 2의 보수 개념을 확장하면 쉽게 정수를 표현할
수 있다. 2바이트 실수를 나타낸다고 생각해 보자. 상위 바이트(high byte)는
소수점 이상 부분을, 하위 바이트(low byte)는 소수점 이하 부분을 나타내면
된다. 이러한 표기법은 소수점의 위치가 고정되어 있어서 고정 소수점(fixed
point) 표기라 한다.

  2.3을 고정 소수점 표기법으로 나타내면, 2는 0000 0010, 3은 0000 0011 이
므로 2바이트 실수를 저장할 때,

0000 0010 0000 0011

로 저장해서, 이것을 해석할 때 상위 바이트를 소수점 이상 부분으로, 하위 바
이트를 소수점 이하부분으로 해석하면 된다. 그러면 이 표기법은 얼마나 큰
수, 또한 얼마나 정밀한 수를 나타낼 수 있을까?

  소수점 이상 부분이 음수를 표현한다면, -128∼127까지를 표현할 수 있다.
소수점 이하부분은 음수를 나타낼 필요가 없으므로, 0∼255까지 표현할 수 있
다. 그러므로 표현할 수 있는 가장 작은 수는 -128.0이며 가장 큰 수는
127.255이다. 또한 가장 정밀한 수는, 0.999는 표현할 수 없으므로, 소수점 이
하 유효자리 2자리까지이다.

  문제의 심각성을 알았는가? 이러한 고정 소수점 표현은 큰 수도 표현하지
못할 뿐더러, 정밀한 수도 표현하지 못한다. 그래서 큰 수도 표현할 수 있고,
정밀한 수도 표현할 수 있는 표현법이 필요하다. 이것이 부동 소수점
(floating point) 표기법이다. 이 세상에 존재하는 모든 컴퓨터는 실수를 표
현하기 위해 부동 소수점 표기법을 이용한다. C에서 실수의 형 이름이 real이
아니고, float인 이유는 여기에 기인한다.

  먼저 소수점의 정규화(normalization)에 대해서 알아보자.

  부동 소수 표현(floating point notation): 진
보된 주제

  실수 역시 2진수로 표현되기는 하지만, 표현 방식은 2의 보수 방법과는 많
이 다르다. 예를 들면, 4바이트 실수(float) -14.24는 컴퓨터에 어떻게 저장될
까?

  아래의 예는 실수 -14.24를 16진수로 인쇄하는 프로그램이다. 프로그램이
구조체(structure), 공용체(union), 표준 스트림(stream) 라이브러리(iostream)
를 사용하고 있으므로, 이해하기가 쉽지 않다. 이 소스는 '공용체'에서 자세히
설명한다. 지금은 프로그램의 출력 결과에 주목하라. -14.24는 16진수로

0xc163d70a

2진수로

1100 0001 0110 0011 1101 0111 0000 1010

이다.

#include <iostream.h>

struct BITFIELD {//이것은 구조체 중에서도 비트 필드 구조체이다.

    unsigned m0:4;//아주 특별해 보이는 이 선언은 m0가 4비트를 차지하는

                  //것을 의미한다.

    unsigned m1:4;

    unsigned m2:4;

    unsigned m3:4;

    unsigned m4:4;

    unsigned m5:4;

    unsigned m6:4;

    unsigned m7:4;

};//struct BITFIELD

union FLOAT {//공용체는 필드(field)를 공유한다. f와 b는 같은 메모리를 공

             //유하며, f의 크기가 32비트, b의 크기 역시 32비트이므로 실

             //제 메모리는 64비트가 아닌 32비트가 할당된다.

    float f;

    BITFIELD b;

};//union FLOAT

void main() {

    FLOAT f;

    f.f=-14.24;

    //cout은 ostream 클래스의 전역 객체이다.

    cout << f.f << endl << hex;//endl과 hex 조작자(manupulator)는 출력

                               //스트림의 형식(format)을 조작한다. 이것

                               //은 '함수 포인터'에서 자세히 다룬다.

    cout << f.b.m0 << f.b.m1 << f.b.m2 << f.b.m3

        << f.b.m4 << f.b.m5 << f.b.m6 << f.b.m7 << endl;

    //output: c163d70a■

             프로그램의 실제 출력 결과는 a07d361c인데 이것은 인텔 CPU가 역워드
          방식으로 수(number)를 저장하기 때문이다. 이 사항은 후에 '연산자'에서
          자세히 다룬다.

}

  결과는

A07D361C

이다.

  이유를 살펴보기 위해, 먼저 실수를 나타내는 방법을 살펴보자. -14.24를 나
타내는 방법은 다양하다. 아래에 몇 가지 예를 나타내었다.

    ...

    -0.01424×103

    -0.1424×102

    -1.424×101

    -14.24

    -142.4×10-1

    -1424×10-2

    -14240×10-3

    ...

  컴퓨터는 이러한 방법 중에 소수점 이상의 자리를 모두 0으로 한 표현법인
-0.1424×102을 사용하는데, 소수점 이하 부분과 지수 부분만을 관리하는 이점
과 큰 수와 정밀한 수를 같이 표현할 수 있는 이점 때문이다. 이렇게 임의의
실수를 유효자리 수가 모두 소수점 아래에 오도록 표기하는 것을 정규화
(normalization)라 한다.

  예를 들면, -14.24를 정규화하면, -0.1424×102이며, 3.141592를 정규화하면
0.3141592×101이다.

  정규화를 하면 소수점의 위치가 부동(floating)하게 된다. 그래서 이러한 정
규화의 결과를 저장하여 실수를 표현하는 방식을 부동 소수점 표현 방식
(floating point notation)이라 한다.

  정규화의 결과를 컴퓨터에 3부분으로 나누어 저장한다. 부호(sign), 지수
(exponential)와 가수(mantissa) 부분이 그것이다. -14.24의 경우 부호는 음
수, 지수는 2, 가수는 1424를 저장한다.

  컴퓨터는 2진수를 사용하므로, 실제의 정규화는 조금 다르다. 모든 C컴파일
러는 4바이트 부동 소수점을 나타내기 위해 IEEE 754 표준 방식(IEEE 32-bit
standard)을 사용하고 있으며, 이것은 제안된 키워드 float를 사용한다. float보
다 2배 정밀도(double precision)■를 가지는 실수는 8바이트(IEEE 64-bit

             4바이트 실수인 float는 단일 정밀도(single precision)라 한다.

standard)이며, 키워드 double을 사용한다. 이 장에서는 float만 다룬다. float
는 부호, 지수와 가수자리를 위해 1,8과 23비트를 사용한다.

             부동 소수 표기: float(IEEE 32-bit standard)는 부호를 위해 1비트, 지수를
          위해 8비트, 가수를 위해 23비트, 모두 32비트를 사용한다.

  float의 정규화 식은 아래와 같다.

(-1)s(1.m)2e-127

  위의 식에서 s,m과 e를 결정하여, 각각의 32비트 자리를 채운다. s가 1이면
(-1)1은 -1이므로 음수를 의미하며, 0이면 (-1)0=1이므로 양수를 의미한다.

  정규화를 하면, 소수점 이하 첫 번째 비트는 항상 1이므로 이 공간을 절약
하기 위해, 처음 1은 항상 있는 것으로 간주하여, m의 자리에 포함시키지 않
는다(이것은 얼마나 섬세한 배려인가!).

  e는 음수를 나타내는 것을 피하기 위하여, 128초과 코드(excess-128 code)■

             이러한 코드 - excess-n code, BCD code, Gray code 등 - 가 어떻게 효
          과적으로 사용되고 있는지는 전산학 개론(introduction to computer science)
          책을 참고하라.

를 사용한다. 예를 들면, e가 0이면, 지수는 -127을 의미하며, e가 128이면 1
을 의미한다.

  이제 -14.24의 s,e와 m을 결정해 보자.

■ 단계 1: s의 결정

  부호는 음수이므로 s=1이다.

■ 단계 2: m의 결정

  먼저 14.24의 2진수를 구해야 한다. 14는 이진수로

1110

             14의 이진수: 14를 계속해서 나눈 나머지를 거꾸로 쓴다.

이며, 0.24는 2진수로

0.0011 1101 0111 0000 1010

             0.24의 이진수: 계속해서 2를 곱해서 소수점 이상 첫째자리를 구한다. 계
          속해서 2를 곱할 때 소수점 이하만 고려한다.

이다. 그러므로 14.24는

1110.0011 1101 0111 0000 1010

이다. 정규화를 하면

1.1100 0111 1010 1110 0001 010×23

이므로, m은 소수점 이하 부분인

1100 0111 1010 1110 0001 010

이다.

■ 단계 3: e의 결정

  정규화의 결과 지수 부분이 3이므로, e=130(e-127=3)이다. 130의 2진수는

1000 0010

이다.

  그래서 위 프로그램의 출력 결과는

C163D70A

이다.

             -14.24의 float 표현: -14.24는 위의 그림처럼 저장된다. 이것을 16진수로
          표현하면, C163D70A이다.

부동 소수의 가감승제(addition, subtraction,
multiplication and division)

  2의 보수 표현의 정수 가감승제는 컴퓨터 구조(computer architecture)등의
책에서 캐리 미리보기 가산기(carry look-ahead adder)등에서 알고리즘을
찾아 볼 수 있다■. 그렇다면, 부동 소수 표현의 가감승제는 어떻게 구현할 수

             이 알고리즘을 직접 구현해 보는 것은 많은 도움이 된다. 예를 들면, 128
          비트 2진수 덧셈을 하려면, 이러한 알고리즘을 직접 구현하여야 한다. 이
          것을 구현한 예는 Alan Parker, "Algorithms and Data Structures in C++",
          CRC Press, 1993 에서 찾알 볼 수 있다.

있을까?

  여기에 관한 대답은 매우 고전적인 것이다. sin(), cos()등의 함수와 더불어
많은 부동 소수 계산 알고리즘이 개발되었다. 그러므로, 자세한 알고리즘은 모
르더라도 이러한 것은 추상화(abstraction)시켜서 생각하면 된다. 하지만, 이러
한 실수 계산이 정수에 비해 많이 느린 것은 사실이다. 이러한 계산을 위한
실수 전용 중앙 처리 장치(CPU)의 가격은 높았으며, 일반 사용자에게는 특
별한 것으로 간주되었다. 그래서 범용 CPU와는 별도로 판매되었으며, 협동
프로세서(co-processor), 수치 연산 프로세서(numerical processor)로 불
렸다.

             볼런드 C++ 3.1의 대화상자: 부동 소수(floating point)를 위해 Emulation을
          사용할지 80387을 사용할지 설정할 수 있다.

  하지만, 펜티엄(Pentium)급 CPU 이상부터는 이러한 협동 프로세서가 범용
CPU에 포함되었다. 486급 이하에서 동작하는 C 컴파일러에서는 부동 소수
계산을 위해 라이브러리를 사용할 지, 협동 프로세서를 사용할지를 설정하는
대화상자(dialog box)를 볼 수 있었다. 하지만, 펜티엄급 이상에서 동작하는
최근 컴파일러에서는 이러한 대화상자를 찾아 볼 수 없게 되었다.

             볼런드 C++ 빌더 3의 Project Options 대화상자: 부동 소수에 대해
          Pentium 60의 한때 버그인 FDIV를 수정하는 체크 박스외에 라이브러리를
          선택하는 부분은 없다. 무조건 펜티엄에 내장된 80487을 사용한다.

  형의 종류(type of types)

    ■ 기본형(fundamental type)

    char

    short

    long

    float

    double

    bool

    wchar_t

    ■ 복합형(compound type)

    array

    function

    pointer

    reference

    class

    struct

    union

    pointer to non-static class member

특별한 형 void

  void가 사용되는 경우는 다음과 같은 4가지 경우이다.

    ■ 함수 선언에서 파라미터 리스트가 비었음을 나타낸다.

    int func(void);//func()는 파라미터를 가지지 않는다.

    ■ 함수가 값을 리턴하지 않는 것을 나타낸다.

    void func(int n);//func()는 값을 리턴하지 않는다.

    ■ 일반 포인터(generic pointer)를 나타낸다. void*는 어떠한 것도 가

    리킬 수 있다.

    void* ptr;//ptr에는 어떤 포인터도 대입할 수 있다.

    ■ 형 변환에서 리턴형을 무시한다.

    extern int errfunc();//에러 값을 리턴한다.

    ...

    (void)errfunc();//리턴 값을 버린다.

  실습문제

1. 아래 프로그램 코드를 Visual C++ 6.0과 Borland C++ 4.5에서 실행시킨 결
과가 있다. 결과의 이유를 자세히 설명하라.

#include <stdio.h>

int i,j;

void main() {

    printf("%p,%p\n",&i,&j);

}

    결과(VC++): 004135CC,004135D0

    결과(16bit BC++): 6B6F:0ACE,6B6F:0AD0

#include <stdio.h>

void main() {

    int i,j;

    printf("%p,%p\n",&i,&j);

}

    결과(VC++): 0064FDF8,0064FDF4

    결과(16bit BC++): 6F6F:2016,6F6F:2014

#include <stdio.h>

void main() {

    int i;

    int j;

    printf("%p,%p\n",&i,&j);

}

    결과(VC++):  0064FDF8,0064FDF4

    결과(16bit BC++): 1117:2016,1117:2014

2.


12. Escape 절차(Sequence)

12. Escape 절차
(Sequence)

  (衁)과 (遁) 두 대의 컴퓨터가 통신을 이용하여 숫자 데이터를 서로 교환한
다고 하자. 이러한 컴퓨터 통신에 사용되는 규약을 통신 규약 혹은 프로토콜
(Protocol)이라 한다.

<그림 ?> 통신 규약(Protocol)

  우리가 사용할 프로토콜은 숫자만 데이터로 사용하므로, a를 시작 바이트,
b를 끝 바이트로 사용하기로 하였다. 그러므로 123을 전송하려면,

a123b

를 차례대로 보내면 된다. 나타내기는 위처럼 나타냈지만, 실제 전송은

b 3 2 1 a

---------、

의 순으로 진행된다.

  그런데 아무런 문제없이 사용하던 이 프로토콜에 문제가 생겼다. 영문자도
데이터에 포함시켜야 하는 일이 발생한 것이다. 단순하게 위의 프로토콜을 사
용하면, 12a3을 데이터로 전송하는 경우,

a12a3b

를 전송하게 되어, a를 구분하는 방법이 모호해진 것이다. 그래서 다음과 같이
프로토콜을 확장하였다.

  (1) 시작 바이트와 끝 바이트는 각각 'ea', 'eb'로 나타낸다.

  (2) e가 데이터에 포함되는 경우는 'ee'를 전송한다.

만약, 전송해야할 데이터가, 12a3인 경우는

ea12a3eb

를 전송하면, e다음에 오는 문자를 확인함으로써 시작 바이트와 끝 바이트를
구분하는 것이 가능해 진다. 보내고자 하는 데이터가 12e3a인 경우는

ea12ee3aeb

를 전송하면 데이터와 시작, 끝 바이트를 구분하는 것이 가능해 진다. 즉 이
데이터를 전송 받는 측에서는 e다음에 오는 문자를 검사하여

  (1) e 다음에 a가 오면 시작 바이트

  (2) e 다음에 b가 오면 끝 바이트

  (3) e 다음에 e가 오면 데이터 e

로 간주하면 되는 것이다. 이러한 것은 사용자에 의해 데이터가 주어지면 e를
덧붙이는 일은 하드웨어 장치에 의해 자동으로 이루어졌는데, 이것을 Esc 절
차(Escape Sequence)라고 한다.

  C++에서 스트링은 이러한 Esc 절차를 지원한다. 어떠한 표현이 Esc 절차를
사용한다고 하면, 그 표현이 사용하는 Esc 문자가 무엇인지를 확인해야 하며,
Esc 다음에 사용하는 약속된 문자에는 무엇이 있는지 확인해야 한다. C++에
서는 '\'를 Esc 문자로 사용하며, 특별히 printf() 같은 몇 함수는 숫자를 나타
내기 위해 '%'도 사용한다. 그리고 Esc 다음에 올 수 있는 약속된 문자에는

n, a, b, t, v

등이 있다. 예를 들어 "\aa\\\nn"을 해석해 보면,

\a + a + \\ + \n + n

이므로,

"삑 소리나고, 'a' 출력, '\' 출력, 줄 바꾸고, 'n' 출력"

을 의미한다. 이러한 Esc 절차의 개념은 여러 곳에 다양하게 응용되어 사용되
므로 개념을 반드시 익혀두어 여러분의 응용에 사용할 수 있어야 한다.

프린터 제어 언어(Print Control Language: PCL)

 아래의 루틴은 VGA 표준 640×480, 16 컬러모드를 레이저 프린터로 출력하
는 함수이다. 이러한 프린터를 제어하는 언어를 PCL이라고 하는데, PCL의
여러 가지 기능은 Esc 절차로 제어한다. 예를 들어 해상도를 설정하는 Esc
절차는

Esc + * + t + 해상도 + R

이므로, 해상도를 100으로 할 경우, C++의 문자열로는

"\0x1B*t100R"

을 전송하면 되는 것이다.

int HPLaserPrint(int x1, int y1, int x2, int y2,

                 int DPI, int PORT_NUM, int LEFT_SKIP,

                 int VERTICAL_SKIP)

/* Hewlett Packard laser printing routine

   Maybe compatible for any laser printer

   Working for VGA standard only. So [x, y] = [ (0~639), (0~479)]

   DPI : 75,100,150,300

   PORT_NUM : 1,2,3,4

   LEFT_SKIP : some bytes

   VERTICAL_SKIP : some bytes

   N.B. 1xxx pixel will be printed i.e. a bit on plane 3 will be

   printed */

{

    unsigned char pattern[10];

    int i, j;

    if (PORT_NUM == 0) PORT_NUM=1; // LPT 1

    if (_bios_printer(_PRINTER_STATUS,0,PORT_NUM-1) & 0x8)

        return -1;//IO error

    if (_bios_printer(_PRINTER_STATUS,0,PORT_NUM-1) & 0x20)

        return -2;//Out of paper error

    if (!(_bios_printer(_PRINTER_STATUS,0,PORT_NUM-1) & 0x10))

        return -3;//not selected

    // fprintf(stdprn, "\33%d", 9); // clear side margins

    // fprintf(stdprn, "\33&a%dL", 0); // Left side margins

    // fprintf(stdprn,"\33*p%dx%dY",x,y); // Cursor positioning

    // fprintf(stdprn, "\33*b%dY",1); // Relative vertical pixel

                                      //movement

    fprintf(stdprn, "\33*t%dR", DPI);  // resolutions 75,100,150,300

    fprintf(stdprn, "\33*r1A");

      //Start graphics at 1:current cursor position, 0:left most

      //position

    for (i=y1;i<=y2;i++) {

        fprintf(stdprn,"\33*b%dW", (x2-x1+1)/8+LEFT_SKIP);

            //Transfer Graphics : number of bytes

        for (j=0; j < LEFT_SKIP; j++)

            biosprint(_PRINTER_WRITE, 0, PORT_NUM-1);

        for (j=x1/8; j < (x2-x1+1)/8; j++) {

            getimage(j*8, i, j*8+7, i, pattern);

            biosprint(_PRINTER_WRITE, pattern[4], PORT_NUM-1);//send

                                                            //a byte

        }//for                                ^Image head skip

    }//for

    for (i=0; i < VERTICAL_SKIP; i++) {

        fprintf(stdprn,"\33*b%dW", 1);//Transfer Graphics : number of

                                      //bytes

        biosprint(_PRINTER_WRITE, 0, PORT_NUM-1);

    }//for

    fprintf(stdprn,"\33*rbC");//End graphics

    return 0;

}//HPLaserPrint

  자세한 설명은 하지 않았지만, 위의 함수를 HP DeskJet등의 프린터에도 바
로 이용할 수 있다. 이제 우리가 직접 Esc 절차를 설계해 보자.

특정한 문자를 강조(Highlight)하기

  "&Hello"라고 표현한 경우, H를 특별히 강조하여 찍고, 나머지는 평범한 회
색을 출력하는 문자열 출력 함수를 만들려고 한다. 즉 &가 Esc 문자로 사용
된 것이다. 이것을 구현한 소스는 아래에 리스트 하였다.

typedef unsigned char byte;

char far *videoram = MK_FP(0xb800,0);

void _putchxy(int x,int y,byte c,byte fore,byte back,byte blink)

{

    byte attr;

    int i=(y-1)*160+(x-1)*2;

    attr=fore & 0x00ff;

    attr=attr | ((back << 4) & 0x70);

    attr=attr | ((blink != 0) << 7);

    *(videoram+i)=c;

    *(videoram+i+1)=attr;

}

int puthistr(int x,int y,char s[],byte fore,byte back,byte hi)

{

    int i=0,tx=x,ret=-1;

    while (i < strlen(s)) {

    if (s[i]=='&') {

        ++i;

        _putchxy(tx,y,s[i],hi,back,0);

        ret=toupper(s[i]);

    } else

        _putchxy(tx,y,s[i],fore,back,0);

    ++i;

    ++tx;

    }

    return ret;

}

  그렇다면 위의 소스를 이용하여 팝업 메뉴(Popup Menu)를 만들어 보자. 우
리가 설계할 메뉴는 Esc 절차를 이용하여 특정 문자를 강조할 뿐만 아니라,
그 문자를 접근키(Access Key)로 등록한다. 예를 들어 메뉴 항목이

File

처럼 출력이 되었다면, 메뉴가 활성화 되었을 때, 'F'를 누르면 그 메뉴 항목
이 선택된다. 물론 방향키를 이용하여 선택할 수도 있다. 소스를 아래에 리
스트하였다.

<그림 ?> 실행 화면

#include <stdio.h>

#include <conio.h>

#include <stdlib.h>

#include <string.h>

#include <dos.h>

#include <ctype.h>

#define KEY_HOME      71

#define KEY_UP        72

#define KEY_PGUP      73

#define KEY_LEFT      75

#define KEY_RIGHT     77

#define KEY_END       79

#define KEY_DOWN      80

#define KEY_PGDN      81

#define KEY_ENTER     13

#define KEY_ESC       27

#define MAX_ITEM      10

#define MAX_LEN       30

#define putchxy(x,y,c)     _putchxy(x,y,c,7,0,0)

#define putsxy(x,y,s)      _putsxy(x,y,s,7,0,0)

enum {FALSE,TRUE};

typedef unsigned char byte;

char far *videoram = MK_FP(0xb800,0);

char item[MAX_ITEM][MAX_LEN];

int  accesskey[MAX_ITEM];

char MFC=BLACK,

     MBC=LIGHTGRAY,

     MHC=RED,

     HFC=LIGHTGRAY,

     HBC=BLACK;

void _putchxy(int x,int y,byte c,byte fore,byte back,byte blink)

{

    byte attr;

    int i=(y-1)*160+(x-1)*2;

    attr=fore & 0x00ff;

    attr=attr | ((back << 4) & 0x70);

    attr=attr | ((blink != 0) << 7);

    *(videoram+i)=c;

    *(videoram+i+1)=attr;

}

void _putsxy(int x,int y,char s[],byte fore,int back,byte blink)

{

    int i;

    for (i=0;i<strlen(s);++i)

    _putchxy(x+i,y,s[i],fore,back,blink);

}

void clearbox(int x1,int y1,int x2,int y2,byte c)

{

    int i,j;

    for (i=x1;i<=x2;++i)

    for (j=y1;j<=y2;++j)

        _putchxy(i,j,' ',0,c,0);

}

void _box(int x1,int y1,int x2,int y2,byte fore,byte back)

{

    int i;

    for (i=x1;i<=x2;++i) {

    _putchxy(i,y1,'-',fore,back,0);//

    _putchxy(i,y2,'-',fore,back,0);

    }

    for (i=y1;i<=y2;++i) {

    _putchxy(x1,i,'|',fore,back,0);

    _putchxy(x2,i,'|',fore,back,0);

    }

    _putchxy(x1,y1,'+',fore,back,0);

    _putchxy(x2,y1,'+',fore,back,0);

    _putchxy(x1,y2,'+',fore,back,0);

    _putchxy(x2,y2,'+',fore,back,0);

}

void box(int x1,int y1,int x2,int y2,byte fore,byte back)

{

    clearbox(x1,y1,x2,y2,back);

    _box(x1,y1,x2,y2,fore,back);

}

int puthistr(int x,int y,char s[],byte fore,byte back,byte hi)

{

    int i=0,tx=x,ret=-1;

    while (i < strlen(s)) {

    if (s[i]=='&') {

        ++i;

        _putchxy(tx,y,s[i],hi,back,0);

        ret=toupper(s[i]);

    } else

        _putchxy(tx,y,s[i],fore,back,0);

    ++i;

    ++tx;

    }

    return ret;

}

int strlen2(char s[])

{

    int i=0,j=0;

    while (s[i] != '\0') {

    if (s[i] == '&') ++j;

    ++i;

    }

    return i-j;

}

int popup(int x,int y,int nitem,int base)

{

    int maxlen=0,i=0,c=0,ci=0,ret=-4;

    byte eKey=FALSE;

    char *buf;

    while (i<nitem) {

    if (strlen2(item[i]) > maxlen)

        maxlen=strlen2(item[i]);

    ++i;

    }

    buf=(char *)malloc( (maxlen+2)*(nitem+2)*2 );

      //save background

    gettext(x,y,x+maxlen+1,y+nitem+1,buf);

    box(x,y,x+maxlen+1,y+nitem+1,MFC,MBC);

    for (i=0;i<nitem;++i)

    accesskey[i]=puthistr(x+1,y+1+i,item[i],MFC,MBC,MHC);

    puthistr(x+1,y+1,item[0],HFC,HBC,HFC);

    while (1) {

    eKey=FALSE;

    c=getch();

    if (c==0) {

        eKey=TRUE;

        c=getch();

    }

    puthistr(x+1,y+1+ci,item[ci],MFC,MBC,MHC);

          //restore inverted item

    if (eKey==FALSE) {

        for (i=0;i<nitem;++i) {

        if (toupper(c)==accesskey[i]) {

            ret=base+i;

            break; /* exit for loop */

        }

        }

        switch (c) {

        case KEY_ENTER:

            ret=base+ci;

            break;

        case KEY_ESC:

            ret=-3;

            break;

        }

        if (ret != -4) break; /* exit while loop */

    } else { /* When extended key is pressed */

        switch (c) {

        case KEY_UP:

            --ci;

            if (ci < 0) ci=nitem-1;

            break;

        case KEY_DOWN:

            ++ci;

            if (ci>=nitem) ci=0;

            break;

        case KEY_HOME:

            ci=0;

            break;

        case KEY_END:

            ci=nitem-1;

            break;

        case KEY_LEFT:

            ret=-1;

            break;

        case KEY_RIGHT:

            ret=-2;

            break;

        }

        if (ret != -4) break; /* exit while loop */

    }

    puthistr(x+1,y+1+ci,item[ci],HFC,HBC,HFC); /* invert item */

    }

    puttext(x,y,x+maxlen+1,y+nitem+1,buf); /* restore background */

    free(buf);

    return ret;

}

void main(void)

{

    int i;

    strcpy(item[0]," &New     ");

    strcpy(item[1]," &Open    ");

    strcpy(item[2]," &Save    ");

    strcpy(item[3]," Save &as ");

    strcpy(item[4]," E&xit    ");

    i=popup(10,10,5,100);

    printf("%d\n",i);

}

  위의 소스는 잡다한 많은 부분을 구현하였으므로, Esc 절차만 제대로 이해
하였다면, 소스 모두를 이해할 필요는 없다.

실습문제

1. 위에서 문자열을 출력하는 함수 puthistr()은 "H&&ello" 처럼 연속된 & 가
사용된 것은 처리하지 않는다. Esc 절차의 정의에 맞게 동작하도록 puthistr()
함수를 수정하라.

2. 위에서 구현 한 팝업 메뉴는 메뉴 항목을 할당하기 위해 여러 줄의
strcpy() 문장을 사용하고 있다. 이 부분을 가변 인자(Variable Argument)를
사용하여, 항목을 할당 할 수 있도록 SetMenuItem()이란 함수를 추가하라.

3. popup()이 가속키도 지원하도록 Esc 절차를 수정하여 구현하라. 예를 들어,
"&Hello\t@X" 인 경우, 가속키로 Alt+X를 누르면 이 메뉴 항목이 선택되어야
한다. 즉 '\t'를 추가적인 Esc 문자로 사용하라.

4.


포인터, [] 연산자

5. 포인터(pointer), [] 연
산자

  먼저 주소(address)라는 용어를 이해해야 한다. 주소는 메모리의 각각의
셀(cell)에 붙여진 일련번호를 말한다. 디스크의 프로그램은 항상 메모리에 로
드(load)된 다음, 실행(execute)된다. CPU가 메모리에 있는 적절한 데이터를 -
이것이 변수이든, 실행 코드이든 - 가지고 와서 명령을 실행하기 위해서는 이
값을 참조해야 한다. 그러므로 메모리의 각각의 셀에는 일련 번호, 즉 주소가
붙여져 있으며, 우리가 참조하는 변수 이름은 실제로 기계에 의해 주소로 번
역된다.

             그림 5-1	메모리의 주소: 위의 그림에서 주소는 0∼255이다. 그러므로 주소
          를 나타내기 위해 8비트를 사용한다. 2번지에는 100이란 값이 들어 있다. 254번지에
          는 200이 들어 있다. 100이나 200을 정수로 생각해서는 안된다. 100은 해석하기에
          따라 정수, 실수 혹은 주소가 되기도 한다. 우리가 int i=100; 이라고 하면, 프로그래
          머의 입장에서는 i에 100이 대입되는 것으로 생각한다. 하지만, 실제로는 컴파일러가
          생성한 i의 위치 - 만약 그것이 2번지라고 하면 - 에 100이 들어가는 것이다. i는 어
          디도 없다. 단지 2번지의 추상화된 이름일 뿐이다.

  메모리 한 셀은 몇 바이트를 의미하는 가? 즉 1000번지는 2개의 바이트를
가리키는가? 아니면, 1바이트를 가리키는가? 메모리 번지가 바이트 단위로 접
근(access) 가능하면 이러한 기계를 바이트 접근가능 기계(byte accessible
machine)라고 한다. IBM 호환 기종의 PC는 바이트 접근 가능 기계이다.

  포인터는 흔히 배열(array)을 설명한 이후에 설명하지만, 지금이 포인터를
설명하기에 적절한 시기이다. C에서의 포인터 변수는 일반적인 '가리킨다'라는
의미로 쓰인 포인터 변수와는 구분되어야 한다. 일반적으로 정수값이 어떤 부
분을 '가리키기'위해 사용되었을 때, 이러한 변수를 포인터라 한다. 예를 들어
스택(stack)의 윗 부분 - 데이터가 들어갈 부분 - 을 가리키는 정수형 변수를
스택 포인터(stack pointer)라 한다. 문맥상 이러한 의미가 명백하지 않을 때,
포인터 변수를 주소 변수(address variable)라고도 한다.

  포인터에 대한 이해는 다음과 같은 간단한 프로그램의 실제 동작을 이해하
는 것에서부터 시작한다.

    void main() {

        int i,j=2;

        i=j;//이 문장에서 실제로 무엇이 일어나는가?

    }

  위의 프로그램은 j의 값을 i에 대입한다. 문제의 핵심은 여기에 있다. i=j라
는 문장은 j를 i에 대입하는 것이 아니다. 또한 j의 값을 i의 값에 대입하는 것
이 아니다. j의 값을 i에 대입하는 것이다. 즉 등호(=)의 왼쪽에 쓰인 i와 오른
쪽에 쓰인 j를 해석하는 방법은 다르다.

  컴파일러가 실행 코드를 생성할 때, 변수의 주소와 함수 호출의 적절한 코
드를 생성하기 위해, 심벌 테이블(symbol table)을 생성하여 유지한다1). 물

  1) 일반적으로 컴파일러는 소스 코드를 두 번 스캔(scan)한다. 첫번째 스캔에서 변수의

    주소와 함수의 주소 등을 해결하기 위하여, 심벌 테이블을 만든다. 두 번째 스캔에서

    실제의 코드를 생성한다. 이러한 컴파일러를 2-패스 컴파일러(2-pass compiler)라 한

    다.

론 이 테이블은 코드를 생성하기 위해(compile-time) 만들어져서, 실행할 때
(run-time)에는 필요가 없으므로 사용하지 않는다.

  main()안에서는 두 개의 정수형 변수가 선언되었다. 컴파일러가 계산한 i,j
의 메모리 주소(memory address)가 각각 100, 102였다고 하자2). 그러면 컴파
  2) 변수의 주소가 몇 번지가 될는지는 예측할 수 없다. 컴파일러는 최적의 코드를 생성

    하기 위해 적절한 변수의 주소를 결정한다. 100을 정수 100으로 생각해서는 안 된다.

    언어에 의해서 정의되는 포인터 표현 100을 의미한다. 여기에서 100은 베이스(base)의

    값이 CS(code segment), 변위(offset)의 값이 100임을 의미한다. 또한 j의 값은 반드시

    102가 되어야 한다. 정수는 2바이트를 차지하므로, i다음의 j위치는 2바이트 이후의 주

    소 값이다.

일러는 코드를 생성할 때 다음과 같은 심벌 테이블을 유지한다.

변수/함수 이름

실제 주소

형

i

100

int

j

102

int

  그림 5-2 심벌 테이블3)

  3) 실제의 심벌 테이블에는 속성(attribute)이 3개만이 아니다. 지금은 설명을 위해 3개의

    속성만을 나타내었다.

j=2; 라는 문장에 의해 102번지에 2라는 값이 들어간다.

   주의 사항

  이 예에서 i가 100번지라면, j는 실제 998번지에 할당된다. 즉 블락안에서는
늦게 선언된 변수가 낮은 번지를 가지게 된다. 이것은 블락안에서 선언된 변
수는 스택에 할당되기 때문인데 스택은 높은 번지에서 낮은 번지로 자란다.
만약 i와 j가 블락의 밖에서 전역 변수로 선언되었다면, j는 102번지에 할당될
것이다. 전역 변수는 메모리의 낮은 곳에서 높은 곳으로 할당된다. 하지만, 이
책에서는 문맥을 해치지 않는 범위에서 스택에 할당된 지역 변수이든, 전역
변수이든 간에 메모리의 낮은 곳에서 높은 곳으로 할당된다고 가정할 것이다.

  그림 5-3 j=2; 의 실행후 메모리의 상태

  i=j; 라는 문장에 의해 무슨 일이 일어나는가? 등호의 왼쪽 i는 심벌 테이블
의 100을 의미한다. 정수와 주소를 구분하기 위해 이를 [100]으로 쓰기로 하
자. 또한 등호의 오른쪽 j는 심벌 테이블 [102]가 가리키는 값을 의미한다. 이
를 [102]*라고 쓰기로 하자. 그러면 i=j; 는 아래와 같은 문장으로 번역됨을 알
수 있다.

[100]=[102]*

  등호의 왼쪽에 오는 값은 반드시 주소(address)라야 한다. 이를 왼쪽 값
(LValue; left value)라 한다. 등호의 오른쪽에 오는 값은 반드시 값(value)
이라야 한다. 이를 오른쪽 값(RValue; right value)라 한다. 다음의 문장은
LValue의 제약을 어긴 에러이다.

3=4;4)

  4) 이 문장을 컴파일해 보라. "LValue required"란 에러메시지가 출력된다.

  그렇다면, i의 주소값인 [100]을 메모리에 저장하는 방법은 없을까? 메모리
에 정수값이 들어가면 정수형 변수이다. 주소값이 들어가면 주소형 변수라야
할 것이다. 이러한 주소형 변수를 포인터(pointer)라 한다.

  만약 저장하고자 하는 주소가 정수를 가리킨다면, 정수형 주소 변수를 선언
하여야 한다. 주소변수는 형 이름과 변수 이름 사이에 asterisk(*)를 삽입하면
된다. 정수형 주소변수는 다음과 같이 선언한다.

int * ip;

  위의 문장과 char * ip; 와의 차이점은 무엇인가? 어차피 포인터 변수라면
같은 것이 아닌가? 이 문제는 일단 뒤로 접어두기로 하자. 여러개의 포인터
변수를 선언하기 위해서는 변수 이름마다 *를 붙여주어야 한다.

int *ip, *ip2;

  변수가 주소형 변수로 선언되지 않았을 때, 실제로 이 변수가 위치하는 메
모리의 위치를 알 필요가 있다. 이것은 주소 연산자(address-of operator;
&)로 가능하다. 주소연산자는 변수 이름 앞에 ampersand(&)를 붙이면 된다.
그러므로 &i는 [100]을 의미한다. 하지만, i=&j; 라는 문장은 가능하지 않다.
정수형 변수 i에 주소값을 대입하는 것은 가능하지 않다. i는 int *i; 로 선언
되어야 할 것이다. int *i; 라고 선언되었을 때, &i는 i값(주소값)의 실제 주소
값이다. i는 값(주소값)이다. *i는 i값이 가리키는 값(정수값)이다. 여러분은 포
인터 변수를 선언하는 시점의 *와 사용하는 시점의 *를 구분하여야 한다. 선
언할 때, *는 그 변수가 포인터 변수임을 의미한다. 하지만 사용할 때 *는 '포
인터 변수가 가리키는 값'을 의미한다. *는 메모리를 두 번 참조하여야 한다.
그래서 *를 간접 지정 연산자(indirect operator)라 한다.

  아래의 예제를 고려해 보자.

    void main() {

        int i,j=2;

        int *ip;//*는 ip가 포인터 변수임을 의미한다

        i=j;//이 문장에서 실제로 무엇이 일어나는가?

        ip=&i;

        printf("%d,%d,%p,%p\n",j,*ip,ip,&ip);//*는 간접지정연산자이다

    }

  만들어진 심벌 테이블은 다음과 같다.

변수/함수 이름

실제 주소

형

i

100

int

j

102

int

ip

104

int *

  그림 5-4 심벌 테이블

  메모리의 구조는 다음과 같다.

  그림 5-5 메모리의 구조

  위의 상황에서 ip=[100]이다. &ip=104이다. *ip는 [100]*이므로, 2이다.
printf()에서 지원하는 Escape 시퀀스는 포인터를 출력하기 위해서 %p를 사용
한다. 그러므로 위 프로그램의 실행결과는

2,2,[100],[104]

이다. 실제로 [100], [104]는 주소값이 16진수로 출력된다. 메모리 모델에 따라
0x????:0x????5) 혹은 0x????형태로 출력된다.6)

  5) ?는 와일드 카드(wild card)를 의미한다.

  6) 0x????형태로 출력되는 포인터는 16비트 포인터, 즉 가까운 포인터(near pointer)라

    한다. 0x????:0x????형태는 32비트 포인터, 먼 포인터(far pointer)라 한다. 메모리 모델

    에 상관없이 가까운 포인터를 선언하려면 char near * i; 처럼 선언한다. 먼 포인터인

    경우, char far * i; 처럼 선언한다. near와 far는 포인터 선언 변경자이다.

  문자열(string)은 포인터 표현이다.

  C의 문자열은 포인터로 관리한다. 문자열 끝에 문자열의 끝(end of
string; EOS)을 나타내는 특수문자 0을 넣는다7). 0은 제어 문자(control

  7) C언어는 EOS를 사용하여 문자열의 끝을 표현한다. 하지만, Pascal 같은 언어는 문

    자열의 선두에 문자열의 길이를 집어 넣어 문자열을 관리한다. 그러므로 Pascal에서

    문자열의 길이를 구하는 함수는 배열의 첫번째 요소를 구함으로서 구현한다. 하지만 C

    에서는 문자열의 선두 포인터로부터 '\0'을 만날 때까지의 문자의 개수를 구하여야 한

    다.

character)이므로, '\0' 처럼 표현하기도 한다. 독자들은 '\0'자체가 문자열에
포함되는 경우를 염려하지 않아도 된다. '\0'은 절대로 문자열에 포함될 수 없
으므로 EOS 표시로 '\0'은 훌륭하게 사용된다.

  다음 문장을 고려하여 보자. s의 형은 무엇이 되어야 하는가?

s="hello";

  문자열이 포인터로 관리된다는 점에 주의하자. 그러므로 s는 다음과 같이
선언되어야 한다.

char *s;

  그렇다면 s에는 도대체 무슨 값이 들어가는가? 컴파일러는 s="hello"; 문장
을 다음과 같이 해석한다. 메모리의 남는 영역  - 일반적으로 힙(heap)이다.
하지만 어떤 컴파일러는 문자열을 위하여 특정 메모리를 예약해서 사용하기도
한다 - 에 "hello"+'\0'을 차례대로 집어넣는다. 문자 5개의 0을 포함하여 6바
이트를 사용한다. 그리고 첫 번째 문자 'h'의 시작 주소를 돌려준다. 그러므
로, "hello"는 'h'의 시작 주소 표현이다. 그러므로 s는 char *s; 처럼 선언되
어야 한다.

  일반적으로 초보자들은 s="hello"; 란 문장을 해석하기를 "문자열 hello를 s
에 대입한다."라고 알고 있다. 하지만 그것이 아니다. 올바른 해석은 다음과
같다.

"hello"+'\0'를 메모리의 남는 영역에 집어 넣은 다음, 첫번째 문자
'h'의 시작 주소를 s에 대입한다.

  그림 5-6 s="hello";의 메모리의 상태

  그렇다면 *(s+1)은 무엇을 의미하는가? s가 1000번지이므로 [1001]*를 의미
한다. 즉 문자 'e'이다. *(s+1)은 연산자 []를 사용하여8), s[1]로 나타낼 수 있

             8) []의 정확한 이름은 배열 첨자 연산자(array subscript operator)이다. 하지만 이
          책에서는 이것을 내용 연산자(content-of operator)라 부를 것이다. exp1[exp2]는 컴
          파일러에 의해 *((exp1)+(exp2))로 번역된다.

다. 다음을 기억하여 두자.

*(s+n) ≡ s[n]

  s와 n의 위치를 바꾸어 *(n+s)라고 쓸 수 있듯이, n[s]라고 쓸 수 있음에 유
의하자. 그러므로 s[1]은 1[s]라고 쓸 수 있다. 다음의 예제는 여러분이 연산자
[]의 역할을 이해하는데 많은 도움이 되리라 믿는다.

#include <stdio.h>

void main() {

    char *s;

    s="hello";

    printf("%c,%c,%c,%d\n",*(s+1),s[1],1[s],s[5]);

    //      e,e,e,0 이 출력된다.

}

  진보된 주제

  포인터의 다른 주제는 "포인터 II" 에서 다룬다. 함수 포인터에 관한 사항
은 "함수 포인터"를 참고하라. 이것은 어렵지만 반드시 넘어야 할 산이다. 포
인터를 이용한 동적 배열의 생성은 "동적 2차원 배열 만들기"를 참고하라.

  또한 C++의 동적 메모리 할당 연산자 new와 delete가 사용될 때는 언제나
포인터가 사용된다.

  C언어의 전성기 시절, 포인터는 가장 중요하고 이해하기 어려운 주제였다.
C++의 클래스가 중요해진 지금 시점에서도 포인터는 여전히 중요하고, 이해
하기 어려운 주제이다. 과장되게 말한다면, 독자들이 포인터를 완벽하게 이해
한다면, C++언어의 50%이상을 이해한 셈이다.

  연습문제

1.


6. 문장 vs 표현식

6. 문장(statement) vs 표
현식(expression)

  if문(if statement)은 다음과 같은 문법을 가진다. 이 문장이 틀렸다는 것을 이
미 알고있는 사람은 이 장의 내용을 건너뛰어도 좋다.

if (비교문장1)

  문장1;

[else if (비교문장2)

  문장2;][...]

[else

  문장3;]

  if 문은 첫 번째 비교문장1을 검사하여, 문장이 참(true)이면, 문장1;을 수행한
다. 첫 번째 비교문장1이 거짓이면, else if에 명시된 비교문장2등을 차례대로 검
사하여, 문장이 참인지를 검사한다. 검사의 결과가 참이면, 문장2;를 수행한다. 명
시한 모든 비교문장들이 거짓이면, 마지막 else에 명시된 문장3을 수행한다.

  여러분은 else if와 else를 둘러싼 '['와 ']'에 주목해야 한다. 이것은 이러한 문
장이 생략가능(option)함을 나타낸다. 그러므로 else if와 else구조는 생략 가능하
다. 여러분은 또한 else if의 마지막에 [...]를 주목하라. 이것은 else if문장이 0번
이상 여러 번 반복(repetition)될 수 있음을 의미한다. 이러한 표기법을 사용하는
것은 복잡한 문법을 체계적이고 형식적(formal)으로 설명할 수 있으므로, 많이
사용한다1).

  1) 지금 당장 도스 프람프트(prompt)에서 'path /?'를 입력해 결과를 확인해 보라. 또한

    arj.exe를 가진 독자는 'arj /?'를 입력해 보라. 이러한 명령들의 사용법은 위에서 제시한 표

    기법으로 표기하고 있다. 수직 바(vertical bar; |)는 혹은(or)을 의미한다.

  이제 몇 가지 if문의 예제를 통해 복잡한 if문을 연습해 보자. 아래 프로그램의
출력 결과는 얼마인가?

#include <stdio.h>

void main() {

    int i=2,j=3;

    if (i>j)//①

        i=i+1;

        if (i<j)//②

            j=j+1;

    else//③

        printf("%d\n",i);

}

  처음 보는 연산자 '>2)'는 수학에서와 같은 의미를 지닌다고 가정하면, ①번 문
  2) ~보다 크다(greater than)라고 읽는다.

장에 의해 i>j라는 조건문장3)은 결과가 거짓이다. 그러므로 ③번 else문이 실행

  3) 이 장의 주제는 i>j가 문장이 아니라는 것을 말하려는 것이다.

되어 화면에 2가 인쇄된다. 직접 실행해 보라. 화면에는 아무 것도 인쇄되지 않
는다. 문제는 여기에 있다.

"③번의 else문이 ②번 if와 짝 지워진 것인지, ①번 if와 짝 지워진 것인지 어떻
게 아는가?"

  즉 소스를 아래와 같이 배열해 보자.

#include <stdio.h>

void main() {

    int i=2,j=3;

    if (i>j)//①

        i=i+1;

    if (i<j)//②

        j=j+1;

    else//③

        printf("%d\n",i);

}

  이러 애매한 문제를 해결하는 규칙을 여러분은 알고 있을 지도 모른다. 어느 if
에 걸려있는지(dangling) 애매한 else문제를 달랑거리는 else문제(dangling else
problem)라고 한다. 지구에 존재하는 모든 컴퓨터 언어는 이러한 문제를 해결하
는 같은 규칙을 사용하고 있다. 규칙은 다음과 같다.

"else의 위쪽으로 봐서, else와 짝 지워지지 않은 가장 가까운 if 혹은 else
if와 짝 짓는다."

  이러한 규칙에 의하면, ③번의 else는 ②번과 짝 지워지므로 j값이 1증가할 뿐,
화면에는 아무 것도 출력되지 않는다. ③번의 else가 ①번과 짝 지워지도록 하는
방법은 없는가? 그것은 ②번의 if문장을 하나의 독립된 문장으로 만듦으로써 가
능하다. 여러분은 여러 개의 문장을 하나의 문장으로 만드는 기능에 대해서 알고
있을 것이다. 블락(block; '{'와 '}'에 둘러쌓인 문장들)은 여러 개의 문장을 하나
의 문장으로 만들어 준다. 그러므로 원래의 의도대로라면 아래와 같이 블락을 사
용하여 소스를 수정해야 한다. 화면에는 2가 출력될 것이다.

#include <stdio.h>

void main() {

    int i=2,j=3;

    if (i>j) {//①

        i=i+1;

        if (i<j)//②

            j=j+1;

    } else//③

        printf("%d\n",i);

}

  하지만 말하려는 주제는 이것이 아니다. 컴파일러는 if의 비교문장이 참인지를
어떻게 검사하는가? 먼저 문장(statement)과 표현식(expression)은 명확히 구
분되어야 한다. 문장 자체의 값이 C에서 지원하는 데이터 값 - 일반적으로 정수
- 인 경우, 이러한 문장을 표현식이라 한다. 물론 표현식은 문장이다. 하지만, 문
장은 표현식이 아니다. 그러므로 if의 괄호 안의 '비교문장'은 틀린 표현이다. 문
장은 결과 값으로 참(true)이나 거짓(false)을 가질 수 없다. 그렇다면 도대체 무
엇이란 말인가? 먼저, 다음의 예들에서 문장과 표현식을 구분해 보라.

    int i=2,j=3;//(1)

    i+j;//(2)

    i=2+3;//(3)

    i=j=2;//(4)

    return i;//(5)

  문장은 모두 2개이다. 표현식은 나머지 3개이다. 컴파일러가 (2)번 문장4)을 컴
  4) 물론 (2)번은 표현식이다. 하지만, 의미의 혼동이 없을 경우 표현식을 일반적으로 문장이

    라 하겠다.

파일하지 못하는 것은 아닌가? 아니다. 표현식은 문장 자체가 숫자(number)임에
주목하라. 그러므로, 문장을 수식의 오른쪽에 서서, 왼쪽에 대입 가능하면 그 문
장은 표현식이다.

  (1)번 문장을 고려해 보자. int i=2,j=3;은 숫자가 아니다. 그러므로

k=(int i=2,j=3);

이란 문장은 가능하지 않다. (5)번 문장 마찬가지이다. return i;는 i란 값을 리턴
함에도 불구하고, 문장 자체가 값은 아니다. 그러므로 k=(return i);는 불가능하
다. 나머지 (2),(3)과 (4)는 모두 표현식이다.

  i+j가 숫자임에 주목하라. 그러므로, k=(i+j);는 가능하다. 또한 i=2+3이 표현식,
즉 숫자임에 주목하라. 이 문장은 2+3을 i에 대입하여, i의 값을 5로 만들며, 마지
막에 대입된 값, 즉 5가 이 문장의 값이 된다5). 그러므로 i가 5이므로, 문장의 값

             5) 물론 수학에서는 이것은 표현식이 아니다. 대입문(assignment statement)은
          숫자가 될 수 없다.

이 5가 아니라, i=2+3이 숫자 5인 것이다. 그러므로 i=j=2;란 문장은 2를 j에 대입
하고, j의 값을 i에 대입하는 것이 아니다. i=j=2;라는 문장의 의미는 다음과 같다.

"2를 j에 대입하고, j=2를 i에 대입한다. 물론 j=2는 2이다."

  아래에 다시 쓴 문장은 여러분의 이해에 도움이 될 것이다.

i=(j=2);

  C를 배운 많은 사람들이 이러한 문장을 잘못 이해하고 있다. 그렇다면 이제 if
문의 문법을 맞게 새로 적어보자.

if (표현식1)

  문장1;

[else if (표현식2)

  문장2;][...]

[else

  문장3;]

  그렇다면, if 괄호 안의 표현식이 참이다, 거짓이다의 판단기준은 무엇인가? C
에는 참/거짓이란 값은 존재하지 않는다. 컴파일러는 참과 거짓을 다음과 같이
구분한다.

  (1) 숫자 표현식 0은 거짓이다.

  (2) 0이외의 모든 표현식은 참이다.

  그러므로, 1은 참이다. 100, -100, -1은 모두 참이다. 0만이 거짓이다. 0을 2의
보수로 표현했을 때, 모든 비트가 0이 됨에 주의하라. 0이외의 정수는 2의 보수
표현에서 적어도 1비트가 1임에 주목하라. 정확하게 말하면, 컴파일러는 참과 거
짓을 다음과 같이 구분한다.

"모든 비트가 0이면 거짓이다. 최소한 1비트라도 1이면 참이다."

  그렇다면, 우리가 위에서 첫번째 예로든 소스에서 i>j가 표현식, 즉 결과가 0혹
은 다른 숫자란 말인가? 그렇다. 여기에 이 장의 핵심이 있다.

  컴파일러는 관계 연산의 결과를 판단하기 위해, i>j에 대해, i-j를 수행한다. 결
과를 검사하여, 모든 비트가 0이면, i>j를 0으로 평가한다. 그렇지 않으면, i>j를
1로 평가한다. 즉, i>j라는 문장은 결과가 0혹은 1인 표현식인 것이다. 그러므로
컴파일러가 if (i>j) 문장;을 검사할 때 괄호안의 조건이 참이기 때문에, 문장;을
수행하는 것이 아니라, 괄호안의 값이 1이기 때문에 문장을 수행하는 것이다.

  이러한 관계 연산자에 대한 이해가 프로그램을 얼마나 강력하게 바꿀 수 있는
지 알아보자.

  i와 j값을 비교하여, i>j인 경우는 k에 1을 대입하고, 아닌 경우는 0을 대입하
는 문장이 필요하다고 해보자. 독자들은 if문을 사용하여 아래와 같이 소스를 작
성할 수 있을 것이다.

    if (i>j)

        m=1;

    else

        m=0;

하지만, 관계 연산자의 특징을 이용하면, 다음과 같이 문장을 간단히 쓸 수 있다.

m=(i>j);

  i가 j보다 크면 1을 리턴하는 함수를 만든다고 해 보자. 역시 다음과 같은 문장
을 사용하여 함수를 구성할 수 있다.

return i>j;

  독자들은 printf("abc");라는 함수 호출이 표현식이라는 것을 알고 있는가?
printf()는 자신이 출력한 문자의 갯수를 리턴한다. 그러므로 아래 문장의 출력결
과는 abchello3이다.

printf("hello%d",printf("abc"));

  마지막으로 아래 프로그램의 출력결과가 무엇이 될지 적어보자.

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    printf("%d,%d,%d,%d\n", i>j, i==j, j!=k, i<=k);

}

  이 장에서 말한 주제를 다시 한 번 정리하면 다음과 같다.

"C에서의 모든 제어구조 - if, while, do... while, switch, for - 의 문장에서
사용하는 비교 문장은 그 문장이 참인 것을 검사하는 것이 아니다, 그 문장의
값이 0이 아닌지를 비교한다. 비교 문장이 1이기 때문에 실행되는 것도 역시
아니다. 문장의 결과가 0이 아니기 때문에 - 1인 경우도 포함하여 - 조건
검사가 참이 되는 것이다."

  문장의 종류

  C++언어의 문장의 종류는 다음과 같다. 표현식은 문장의 부분집합(subset)이
다.

    ■ 라벨문(labeled statement)

    ■ 표현식(expression statement)

    ■ 복합문(compound statement)

    ■ 선택문(selection statement)

    ■ 반복문(iteration statement)

    ■ 분기문(jump statement)

    ■ 선언문(declaration statement)

    ■ try-블락문(try-block statement)

  실습문제

1.


7. 연산자

7. 연산자(operator)1)

    1) operator의 영어 발음에 유의하라. 첫 음절에 강세(accent)가 있다. 그러므로 '오퍼레이터'

      라 읽어서는 안 된다. '아퍼레이터'라 읽는다.

  연산자에 관한 주제는 명확한 설명이 필요하다. 각 연산자의 기능을 이해하고
암기하여야 한다. 그래서 이 장에서는 연산자의 형식을 설명하고, 예제를 통해
사용법을 설명한다. 연산자는 다음과 같은 범주로 구분한다2).

  2) 이 범주는 Borland C++ 4.5의 도움말 파일에 따른 것이다.

    산술(Arithmetic)

    할당(Assignment)

    비트(Bitwise)

    C++에서만 사용가능(C++ specific)

    콤마(Comma)

    조건(Conditional)

    논리(Logical)

    후위표기(Postfix)

    전처리(Preprocessor)

    참조/역참조(Reference/Dereference)

    관계(Relational)

    sizeof

    형 변환(casting)

  C++에서 아래의 연산자들은 오버로드(overload)3) 될 수 없다. 아래의 연산

  3) C++에서는 연산자의 기능을 사용자가 정의할 수 있다. 이것을 연산자 오버로드

    (operator overloading)라 한다.

자들을 제외한 모든 연산자들은 오버로드 될 수 있다.

    .	C++ 직접 요소4) 선택(direct component selector)

  4) 멤버 함수나 멤버 변수를 의미한다.

    .*	C++ 재참조(dereference)5)

  5) 포인터가 가리키는 값을 참조한다. 간접 지정 연산자(indirect operator)라고도 한다.

    ::	C++ 범위 접근/해결(scope access/resolution)

    ?:	조건 연산자(Conditional)

  문맥에 따라, 같은 연산자 기호가 한 가지 이상의 의미를 가질 수 있다. 예를
들면, ampersand(&)는 다음과 같이 해석될 수 있다6).

  6) 언급되지 않은 연산자는 뒤에 자세히 다룬다.

  (1) 비트 AND 연산자

  (2) 주소(address-of) 연산자

  (3) C++에서 참조에 의한 호출 변경자(reference modifier)

  2개의 심벌이 1개의 연산자를 이룰 경우, 2개의 심벌 사이에 공백이 들어가면
안된다. 또한 위치가 바뀌어서도 안된다. 예를 들면, 관계연산자 >=가 > = 혹은
=>로 쓰이면 의미가 달라진다.

  산술(Arithmetic)

  이제부터 설명하는 연산자들은, 특별히 명시되지 않는 한 이항 연산자(binary
operator)7)이다. 실제로 단항(unary)이나 삼항(ternary) 연산자들은 몇 개 되지

  7) 피연산자(operand)의 수가 반드시 2개임을 의미한다.

않는다.

+	단항 부호연산자, 양수(positive number)를 의미한다.

-	단항 부호연산자, 음수(negative number)를 의미한다.

+	덧셈 연산자

-	뺄셈 연산자

*	곱셈 연산자

/	나눗셈 연산자

%	나머지(modula) 연산자

++	단항 전위 증가 연산자

++	단항 후위 증가 연산자

--	단항 전위 감소 연산자

--	단항 후위 감소 연산자

  산술 연산자 +, -, *, /의 의미는 수학적인 의미와 같다. 가감승제 연산이 피연
산자의 형(type)에 따라 다른 연산을 한다는 것에 주의하라. 2+3과 2.0+3.0은 다
르다. 전자의 결과는 5이지만, 후자의 결과는 5.0이다. 독자들은 5의 표현과 5.0의
표현이 메모리 내부에서는 다르다는 것을 알고 있을 것이다. 정수(integer)는 2의
보수로 표현되지만, 실수(floating)는 떠다니는 소숫점(floating point) 표기법으로
표현된다. 덧셈, 뺄셈, 곱셈에서 그 차이를 별로 느낄 수 없지만, 나눗셈에서는
심각하다. 다음의 결과를 주목하라.

#include <stdio.h>

void main() {

    int i=31,j=39;

    int k=10;

    printf("%d,%d\n",i/k,j/k);

    //      3,3

}

  정수 나눗셈의 결과는 소수점 이하는 항상 무시된다. 결과가 3.1이든 3.9이든 3
이다8).

  8) 이것을 flooring라 한다. 수학적인 기호로 ㆓3.9㆔=3처럼 쓴다. ㆃ3.1ㆄ=4는 ceiling을 의미한다.

    3.1이든 3.9이든 4이다.

  나머지 연산자 %는 나눗셈의 나머지를 평가한다. i=10/3의 결과 i는 3이다. 하
지만, i=10%3의 결과 i는 1이다. 10을 3으로 나눈 나머지(modulus)는 1이다. 나머
지 연산자의 용법을 정확히 익혀 두기 바란다. 다음과 같은 문제를 해결하는데
나머지 연산자가 사용된다.

  (1) n의 배수를 구하는 문제

  (2) 화면의 (x,y)좌표의 실제 선형 메모리의 위치를 구하는 문제9)

  9) 이 문제는 연습문제에서 다룬다.

  아래의 예제는 1에서 100사이의 정수 중 3의 배수를 출력한다.

#include <stdio.h>

void main() {

    int i=0;

    while ((++i)<=100)

        if (i%3==0) printf("%d,",i);

}

  증감 연산자는 대입 연산자 =처럼 부효과(side effect)가 있다. 즉, 표현식 자
체가 변수의 값을 변경시킨다. 표현식은 식 자체가 값을 가지며, 어떠한 변수의
값도 변경시켜서는 안 된다. 3+5라는 표현식(expression)은 8이다. 하지만, 어떠한
값도 변경시키지 않는다. i=3, j=5 라고 하자. i+j는 8이다. 하지만, i와 j는 변하지
않는다. 즉 표현식은 식을 평가(evaluate)할 뿐 변수 값을 변경시켜서는 안 된다.
하지만, k=i+j라는 문장을 고려해 보자. i+j라는 식의 평가값이 k를 변경시킨다.
이러한 표현식을 우리는 '부효과가 있다'고 한다. 즉 표현식이 실행되고 난 뒤
바뀌는 변수값이 있다면 그 표현식에는 부효과가 있다.

++i

  위 문장은 i의 값을 1증가시킨다. i의 값이 변하므로 부효과가 있다. 위 문장은
다음 문장과 동일하다.

i++

  독자들은 i를 1증가시키는 문장을 정리할 필요가 있다. 아래의 문장들은 모두 i
값을 1증가시키는 문장이며, 모두 부효과가 있다.

    i=i+1;

    ++i;

    i++;

    i+=1;10)

  10) i+=1을 i=+1과 혼동하지 말라. 전자는 i를 1증가시킨다. 하지만 후자는 i에 양수 1을 대

    입한다. i-=1과 i=-1 역시 마찬가지이다.

  마지막 문장은 아직 다루지 않았지만, 대입 연산자에 의해 i값이 1 증가한다.
증가 연산자를 전위에 붙이는 것과 후위에 붙이는 것은 어떤 차이가 있는가? 독
자들은 이 차이를 명확히 이해하고 있어야 한다. 후에 C++에서 전위 증감 연산
자와 후위 증감 연산자를 오버로딩하는 문법에도 차이가 있다. 아래의 문장을 기
억하라.

"전위 증감 연산자는 식을 평가하기 전에 먼저 증감한다. 후위 증감 연산자는
식을 평가한 후 증감된다."

  i=2,j=3인 경우, i=i+(++j)와 i=i+(j++)는 다르다. 전자인 경우 i=6이다. 후자인 경
우 i=5이다. 물론 둘 다 j=4이다. 그렇다면 여러 개의 변수에 증감연산자가 사용
되었을 때 어떻게 해석하는가? 컴파일러는 ①전위 연산자를 위해 코드를 생성하
고, ②표현식을 위해 코드를 생성한 다음, ③후위 연산자를 위해 코드를 생성한
다. 그러므로 다음과 같은 문장은,

i=i+(++j)-(k--)+(j++)+k;

다음과 같이 번역된다.

    ① j=j+1;

    ② i=i+j-k+j+k;

    ③ k=k-1; j=j+1;

  위와 같은 일련의 문장이 실행되어 i,j,k의 값이 결정된다. 무척 복잡하다. 그렇
다. 증감 연산자의 혼용은 코드를 읽기 어렵게 만든다. 증감 연산자는 위의 예에
서처럼 같은 변수에 대해서, 또 전위와 후위를 썩어서 사용하는 것은 권장되는
사항이 아니다. 증감 연산자를 사용하는 관례(convention)는 다음과 같다.

"한 표현식에서는 같은 형태 - 전위 혹은 후위 - 의 증감 연산자를
사용하라. 같은 형태라도 한 변수에 대해서 1번 이상의 증감 연산을 사용하지
말라."

  위의 예에서는 전위와 후위를 썩어 쓰고 있을 뿐만 아니라, 같은 변수 j에 대
해서 2번씩이나 증감을 하고 있다!

  할당(Assignment)

=	간단한 할당 연산자 외

*=	/=	%=	+=	-=	<<=	>>=	&=	^=	|=

  할당 연산자는 할당 연산자의 오른쪽 표현식의 값을 평가하여 왼쪽 변수에 대
입한다. i=j+k는 j+k의 평가값을 i라는 변수에 대입한다. i값이 변경되므로 부효과
가 있다. 여러분은 할당문의 왼쪽에 있는 변수(left value: l-value)를 해석하는
방법과 오른쪽에 있는 변수(right value: r-value)를 해석하는 방법이 다르다
는 것에 주목할 필요가 있다. i=2, j=3일 때

i=j;

라는 문장을 어떻게 해석하는가? 물론 3을 i에 대입한다. 파악했는가? j를 i에 대
입하는 것이 아니라, j의 값인 3을 i에 대입하는 것이다. 즉 할당문의 오른쪽에
있는 표현식의 값을 왼쪽 변수 - 즉 실제 i가 할당된 메모리의 곳 - 에 할당하
는 것이다. 그래서 왼쪽에는 항상 변수가 와야한다. 하지만 오른쪽에는 값이 와
도 된다. 다음 문장은 이러한 규칙을 위반한 에러이다.

3=j;

이러한 문장은 컴파일러에 의해

"L-value required"

란 에러메시지를 발생한다. ++1같은 경우도 마찬가지이다. 컴파일러는 이 문장을
1=1+1로 해석하려고 하기 때문에 l-value가 틀렸다는 에러메시지를 출력한다.

  많게 보이는 복잡한 할당문을 만드는 규칙을 이해한다면, 이러한 할당문을 사
용하는 것은 무척 쉽다. i=i+j는 i+=j와 동일하다. 이러한 것은 다음과 같은 그림
으로 일반화 된다.

  그림 7-1 복잡한 할당문의 원리

  할당문의 왼쪽과 오른쪽의 첫번째 변수가 같다면, 복잡한 할당문으로 고칠 수
있다. 먼저 오른쪽 표현식에서 중복되는 같은 변수를 없앤다. 그러면 =+형태로
남는다. 이 둘의 위치를 바꾼다.

  i+=j*k가 i=i+(j*k)임에 주의하라. i=(i+j)*k를 복합 할당문을 사용하도록 고칠
수 없다. 오른쪽의 첫번째 변수는 i+j이지 i가 아니기 때문이다. 그러므로

i-=j+k+l;

은 i=i-j+k+l이 아니라, i=i-(j+k+l)이다.

  위에서 제시한 연산자들(*=	/=	%=	+=	-=	<<=
>>=	&=	^=	|=)은 이처럼 복잡한 할당문으로 만드는 것이 가능하
다.

  할당문도 표현식임에 주의하라. i=j;라는 할당문은 j의 값을 i에 대입할 뿐만 아
니라, i=j 라는 문장 자체가 i의 값을 가지게 된다11). 그러므로 다음과 같은 문

  11) 물론 수학에서는 (i=j)는 값이 아니다. 단지 할당문 일뿐이다. 하지만 C에서는 i=j는 표현

    식이다. i=3이라는 문장은 i값을 3으로 바꾼다. 또한 i=3은 3이다.

장을 사용하는 것은 가능하다.

i=j=k=0;

이 문장을 다음과 같이 해석하는 것은 틀렸다.

"0을 k에 대입하고, 0을 j에 대입하고, 0을 i에 대입한다."

오히려, 다음과 같이 해석하여야 한다.

"0을 k에 대입한다. k=0을 j에 대입한다. j=(k=0)을 i에 대입한다."

그러므로 오른쪽의 0이 i에 대입된 것이 아니라, j=k=0의 값 - 물론 이 값은 0
이다 - 이 i에 대입된 것이다.

  할당문을 사용할 때도 반드시 지켜야 할 규칙이 있다. 할당문을 함수의 파라미
터로 사용할 때, 한 개 이상의 할당문을 사용하지 말라. 그리고 할당문이 한 개
사용되더라도, 할당문에서 사용된 변수를 다른 파라미터로 넘기지 말라. i=2; j=3
인 경우,

printf("%d\n",i=j);

는 j값 3을 i에 할당하므로, i값이 3이된다. 또한 i=j라는 할당문 자체의 값이 3이
므로 3을 출력한다. 하지만, 아래의 출력결과에 주목하라.

printf("%d,%d\n", i, i=j);

프로그래머의 의도는 i를 출력하고, i를 j값으로 초기화 시킨다음, i=j를 출력하려
했으므로, 결과가 2,3 이 될것이라고 예측할 것이다. 하지만, 결과는 3,3이다. 이
문제는 뒷장에서 파라미터 전달 방법(parameter passing method)을 이야기
할 때 자세히 다룬다. 어쨋던 기억하라. 함수의 파라미터에 할당문을 되도록이면
사용하지 말라.

  비트(Bitwise)

&	비트 논리곱(bitwise AND12)) 연산자

  12) 모든 비트에 대해서 적용된다는 의미이므로 bit AND가 아니고, bitwise AND란 말이 적

    당하다.

|	비트 논리합(bitwise OR) 연산자

^	비트 배타적 논리합(bitwise exclusive OR) 연산자

~	단항 비트 논리부정(bitwise NOT) 연산자

<<	비트 왼쪽 쉬프트(bitwise shift left) 연산자

>>	비트 오른쪽 쉬프트(bitwise shift right) 연산자

  이 비트 연산자들과 논리 연산자(&&, ||, !)의 용도의 차이에 대해서 주목하라.
비트 연산자는 전체 숫자에 대해서 적용 되는 것이 아니고 각각의 비트에 대해
서 적용된다. 연산자의 역할은 다음과 같다.

&	bitwise AND; 두 비트가 모두 1이면, 1이다. 그외의 경우는 0이다.

|	bitwise inclusive OR; 두 비트가 모두 0이면, 0이다. 그외의 경우는 1이

    다.

^	bitwise exclusive OR; 1의 수가 홀수개이면, 1이다. 그외의 경우는 0이

    다13).

  13) 1^1=0임에 주의하라. 1^1^1은 (1^1)^1=0^1=1이다.

~	bitwise complement; 단항 연산자이므로 피연산자가 1개이다. 0과 1을

    토글(toggle)한다14).

  14) 1의 보수를 구한다.

>>	bitwise shift left; 비트열(bit sequence)을 오른쪽으로 이동(shift)시킨다.

    남는 자리에는 0으로 채워진다15).

  15) 남는 자리가 잘려나간 비트로 채워지는 것을 회전(rotate)이라 한다. 회전은 C의 연산

    자에서 지원하지 않지만, 구현할 수 있다. 연습문제를 참조하라.

<<	bitwise shift left; 비트열을 왼쪽으로 이동시킨다. 남는 자리에는 0으로

    채워진다.

  비트 연산자들의 피연산자의 형은 반드시 정수형 - char, int, long - 이라야
한다. 실수나 포인터에 비트연산자를 적용하는 것은 의미가 없다.

  다음의 진리값 테이블을 참조하라.

E1

E2

E1 & E2

E1 | E2

E1 ^ E2

0

0

0

0

0

0

1

0

1

1

1

0

0

1

1

1

1

1

1

0

  표 7-1 비트 연산자의 진리표

  아래 프로그램의 결과를 조심스럽게 계산해 보라. 또한 반드시 그렇게 할 수
있어야 한다.

#include <stdio.h>

void main() {

    int i=2,//0000 0000 0000 0010

        j=3,//0000 0000 0000 0011

        k=5;//0000 0000 0000 0101

    printf("%d,%d,%d,%d,%d\n", i&j, i|j, i^j^k, i<<2, k>>1);

    //       2  3  4  8  2

}

  독자들은 shift 연산자의 역할에 특별히 주목할 필요가 있다.

i<<n

은 i*2n과 동일하다. 물론 오버플로우(overflow)는 고려하지 않는다.

i>>n

은 i/2n과 동일하다. 물론 언더플로우(underflow)는 고려하지 않는다.

  독자들은 또한 &와 |의 비트 마스크(bit mask)기능에 주목할 필요가 있다. 이
기능은 아무리 강조해도 지나치지 않다. 비트 마스크는 다음과 같은 두 가지를
말한다.

  (1) bit set; 비트열의 특정한 부분을 1로 만든다.

  (2) bit clear; 비트열의 특정한 부분을 0으로 만든다.

  비트열의 특정한 부분을 1로 만들기 위해서는 | 연산자를 사용하고, 비트열의
특정한 부분을 0으로 만들기 위해서는 & 연산자를 사용한다는데 주목하라. 아래
의 예제는 16진 정수 비트열의 11,10,9,8 비트를 0으로 지우고, 7,6,5,4 비트를 1로
초기화한다.

#include <stdio.h>

void main() {

    unsigned int i=0x1234;//0001 0010 0011 0100

    printf("%x\n", i&0xf0ff);

    //       1034

    printf("%x\n", i|0x00f0);

    //       12f4

    printf("%x\n", i&0xf0ff|0x00f0);

    //       10f4

}

  ^ 연산자는 특정한 숫자를 토글시키기 위해 사용할 수 있다. i=0일 경우 아래
의 문장은 0과 1을 토글한다.

i=i^1;

  아래의 ~ 연산의 결과를 정확하게 계산해 보라16).

  16) 3은 2의 보수로, 0000 0011이다. ~3은 비트를 반전시키므로, 1111 1100이다. 이것은 부

    호있는 2의 보수표현에서, 최상위 비트가 1이므로 음수이며, 값은 1111 1100의 2의 보수이

    다. 1111 1100의 2의 보수는 0000 0100이므로 4이다. 그러므로 -4가 값이다.

#include <stdio.h>

void main() {

    char i=3;

    printf("%d\n",~i);

    //       -4

}

  또한, &, >>, << 연산자는 문맥에 민감하다는데 주의하라. &는 주소 연산자
로 혹은 참조에 의한 변수전달 선언으로, <<와 >>는 C++의 표준 클래스 라이
브러리(iostream)에서 출력과 입력 연산으로 오버로딩되어 있다.

  논리 연산자 - &&, || - 혼돈되지 않도록 하기 위하여, 아래의 문장을 기억해
두자.

"비트는 1자리를 의미한다. 그러므로 비트 연산자는 1개의 심벌 - &, | - 을
사용한다."

  비트연산자와 논리 연산자의 결과는 엄청나게 다르다는 것에 주의하라. 아래의
문장은 i와 j의 값이 모두 참인 경우, "hello"를 출력하는 if문이다. i=1이고 j=2인
경우 "hello"는 출력된다17).

  17) 0은 거짓이고, 0이 아니면 참으로 간주한다. 그러므로 -1도 참이다.

    int i=1,j=2;

    if (i && j) printf("hello");

실수로 if문을 다음과 같이 적었다고 하자.

if (i & j) printf("hello");

"hello"는 출력되지 않는다18).

  18) i & j는 0000 0001 & 0000 0010이므로 결과는 0이다.

  C++에서만 사용가능(C++ specific)

  아래와 같은 연산자들은 C++에서만 사용할 수 있다.

::		범위 접근/해결 연산자(Scope access (or resolution) operator)

.*		클래스 멤버의 포인터의 재참조(Dereference pointers to class

        members)19)

  19) 클래스의 멤버가 포인터 변수로 선언되었을 때, 그 멤버 변수가 가리키는 값을 참조한

    다.

->*		클래스 포인터 멤버의 포인터의 재참조(Dereference pointers to

        pointers to class members)20)

  20) 객체의 포인터 변수가 포인터 멤버를 가질 때, 그 변수가 가리키는 값을 참조한다.

new		동적으로 메모리를 할당한다.

delete		동적으로 메모리를 해제한다.

typeid		형이나 표현식에서 실행시간 정보 확인(run-time identification)

        을 얻는다.

dynamic_cast	포인터를 원하는 형(type)으로 변환(cast)한다.

static_cast	포인터를 원하는 형으로 변환한다.

const_cast	형(type)으로부터 const 혹은 volatile 변경자를 추가하거나, 제

        거한다.

reinterpret_cast	안전하지 않거나(unsafe) 구현 의존적인(implementation

        dependent) 변환을 위한 형변환을 대치한다.

typesafe_downcast	객체의 포인터를 클래스의 포인터로 변환한다.

  범위 해결 연산자(::)는 블락 안에서 선언된 변수/함수와 이름이 같은 전역 변
수/함수를 참조할 수 있도록 해 준다. 또한 클래스의 멤버 함수를 정의하기 위해,
클래스의 정적 변수를 참조하거나 정적 멤버 함수를 호출하기 위해 사용된다. 또
한 상속 받은 클래스에서 부모 클래스의 변수/함수를 참조하기 위해 사용된다.
클래스의 멤버 함수를 구현 할 때, 지역 변수와 클래스의 멤버 변수를 구분하기
위해서도 사용된다.

  다음의 예제를 통해 그 사용법을 완전히 익혀 두자. 아직은 설명하지 않은 부
분들 때문에, 이해되지 않는 부분이 있을 것이다. 지금은 넘어 가도 좋다. 하지
만, 후에 ::의 용도를 정리하여 완전하게 파악해 두라.

(1) 전역 변수/함수21)의 참조

  21) 멤버 함수가 아닌 함수를 전역 함수(global function)라 하기로 하자. 일반 함수

    (generic function)란 말이 더 적당할 것 같지만, C++의 일반 함수와 구분하기 위해 전역

    함수란 말을 사용한다.

#include <stdio.h>

int i=1;

void Print() {

    printf("%d\n",i);

}//Print

class CTest {

    int i;

  public:

    CTest(int j) {

        i=j;

    }//CTest

    void Print() {

        ::Print();//일반함수 Print()를 호출한다.

        printf("%d\n",i);

    }//Print

};//CTest

void main() {

    CTest c(2);

    char i=3;

    {

        int i=4;

        printf("%d,%d\n",i,::i);//::i는 전역변수를 참조한다.

    }

    c.Print();

}//main

/*

    4,1

    1

    2   */

  결과를 소스 아래에 설명문으로 적어 두었다. 반드시 실행해 보고, 용도를 익
혀두기 바란다.

(2) 클래스 멤버 함수의 정의

  위의 예에서 CTest의 멤버 함수 Print를 클래스 블락 밖에서 정의하려면, 다음
과 같이 한다. 연산자의 용도는 없으므로, 엄격한 의미에서 연산자라 할 수는 없
다. 하지만, 같은 기호를 사용하고 있으므로, 같이 외워두기 바란다.

    void CTest::Print() {

        ::Print();//일반함수 Print()를 호출한다.

        printf("%d\n",i);

    }//Print

(3) 클래스 정적 멤버 변수(static member variable)의 초기화, 정적 멤버 함수의
호출

  정적 멤버 변수는 객체마다 할당되지 않고 같은 형의 클래스의 객체가 모두
접근할 수 있는 메모리 영역에 할당된다. 정적 멤버 변수를 다음과 같이 초기화
하는 것은 에러이다.

#include <stdio.h>

class CTest {

    static int counter=0;//이 문장은 에러이다

  public:

    CTest() {

        ++counter;

    }//CTest

    static void PrintNOfObject() {

        printf("%d\n",counter);

    }//PrintNOfObject

    ~CTest() {

        --counter;

    }//~CTest

};

  왜 클래스를 정의할 때, 변수의 초기화가 불가능한가? 클래스는 정의문
(definition statement)임에 유의하라. 형을 정의하면서 형의 값을 초기화 할 수는
없다. 비록 클래스의 멤버 변수는 변수처럼 보이기는 하지만, 클래스라는 형을
구성하는 요소이므로, 초기화할 수 없다22).

  22) 일반 멤버 변수가 초기화 될 수 없는 것과 같다. 객체가 만들어지기 전에 값을 가질 수

    는 없다.

  정적 변수를 초기화하는 것과 정적 함수를 호출하는 소스는 아래와 같다.

#include <stdio.h>

class CTest {

    static int counter;

  public:

    CTest() {

        ++counter;

    }//CTest

    static void PrintNOfObject() {

        printf("%d\n",counter);

    }//PrintNOfObject

    ~CTest() {

        --counter;

    }//~CTest

};

int CTest::counter=0;

void main() {

    CTest a,b,c;

    CTest::PrintNOfObject();

}//main

  위 프로그램의 출력결과는 3이다.

(4) 상속 받은 클래스에서 부모 클래스의 변수/함수 참조

#include <stdio.h>

class Base {

    int i;

  protected:

    int j;

  public:

    Base(int t=0) {

        i=j=t;

    }//Base

    void Print() {

        printf("%d\n",i);

    }//Print

};//class Base

class Derived : public Base {

    int j;

  public:

    Derived(int t) {

        j=t;

    }//Derived

    void Print() {

        Base::Print();//Base의 Print()를 호출한다

        printf("%d,%d\n",Base::j,j);//Base의 j를 참조한다

    }//Print

};//class Derived

void main() {

    Derived d(3);

    d.Print();

}//main

  출력결과는

0

0,3

이다.

(5) 지역 변수와 멤버 변수의 구분

#include <stdio.h>

class CTest {

    int i;

  public:

    CTest(int i) {

        CTest::i=i;

    }//CTest

    void Print() {

        printf("%d\n",i);

    }//Print

};//CTest

void main() {

    CTest t(3);

    t.Print();

}//main

  출력결과는 3이다.

  .*와 ->*는 클래스의 멤버 변수의 포인터를 재참조하기 위해 사용한다23).

  23) 클래스의 멤버 변수의 주소는 특별하게 취급되어야 한다. 일반 변수처럼 취급하면, 객

    체마다 할당된 멤버 변수의 주소를 얻을 수는 없다.

아래의 예를 참조하여 개념을 확실히 파악해 두기 바란다.

#include <stdio.h>

class CTest {

    int a,b,c;

  public:

    CTest() { a=b=c=0; }

    friend void SetValue(CTest &t,int i);

    void Print() { printf("%d,%d,%d\n",a,b,c); }

};

void SetValue(CTest &t,int i)

{

    int CTest::*ip;

    ip=&CTest::a;

    t.*ip=i;

}//CTest::SetValue

void main()

{

    CTest t;

    SetValue(t,100);

    t.Print();

}//main

  프로그램의 출력결과는 100,0,0이다. 이 부분이 완전하게 이해되지 않는 독자는
이 부분을 건너뛰어도 좋다. 후에 '멤버 변수의 주소를 얻는 법'에서 자세히 설명
할 것이다. ->* 의 사용 역시 .* 와 동일하다. ->* 를 사용하기 위해서는
SetValue()를 어떻게 수정하여야 하는가?

  new

  C++의 막강한 new에 대해 마침내 설명하게 되었다. 독자들은 새로운 동적 메
모리 할당(dynamic memory allocation) 연산자 new에 심취하게 될 것이다.
유일한 단점이 있다면, new가 예약어(reserved word)가 되는 바람에 C++에서는
변수 이름으로 new를 쓸 수 없다는 것이다!

  C에서는 동적으로 메모리를 할당하기 위해, malloc()이라는 표준 함수를 이용
했다. 예를 들어 10개의 정수를 저장하기 위해서, 메모리를 할당하는 경우 - 20
바이트 할당 - 다음과 같은 문장을 사용한다.

i=(int *)malloc(sizeof(int)*10);

  물론 i는 int *로 선언되어 있어야하며, 포인터 연산의 정확성을 보장하기 위해
24), (int *)를 사용하여, 형 변환(casting)을 해주는 것이 반드시 필요하다. 또

  24) 독자들은 char *와 int *의 차이점을 이해하고 있어야 한다. 동적으로 메모리를 할당하

    는 경우, 포인터의 대상형(dereferenced type)을 알 수 없으므로, 명시적인 형 변환이 반드

    시 필요하다. 이러한 형 변환은 포인터 증감 연산시 포인터의 바른 동작을 보장한다. char

    *와 int *의 차이점은 다음과 같다. char *cp 인 경우, cp=cp+1은 cp가 1증가한다. 하지만,

    int *ip 인 경우, ip=ip+1은 ip를 2증가시킨다.

한 메모리 사용 후 해제 - 할당된 메모리를 다시 사용 가능하다고 운영체제에게
알려 주는 일 - 하기 위하여,

free(i);

라는 문장을 사용한다. malloc()와 free()는 함수이므로, 이것들을 사용하기 위해
서는 alloc.h를 포함시키는 다음과 같은 문장이 필요하다.

    #include <alloc.h>

  먼저 명시적인(explicit) 형 변환(type conversion)이 왜 필요한지 알아보자. 아
래 프로그램의 출력 결과는 얼마인가?

#include <stdio.h>

#include <alloc.h>

void main() {

    int a[]={0x1234,0x5678,0x9012};

    int *ip=a;

    char *cp=(char *)a;

    printf("%x,%x\n",*(ip+1),*(cp+1));

    //       5678,12

}//main

출력결과는 놀랍게도 5678,12 이다! 여러분은 이 프로그램의 결과를 이해하기 위
해서 먼저, 인텔 CPU의 역워드(inverse word)25) 구조에 대해서 이해하고

  25) 이것을 little endian - 이진수 표현에서 가중치가 작은 쪽(little)이 메모리에서 먼저 나타

    난다는 의미로 - 이라고도 한다. little endian이 아닌 경우 big endian이라고 하는데,

    Motorola 계열의 CPU가 이 경우에 속한다.

있어야 한다. 여러분은 위 프로그램의 메모리 상태가 아래 그림과 같다고 생각할
것이며, 또한 사실이다.

  그림 7-2 메모리 상태

  int형의 배열이 1000번지에 할당되었으며, 각각의 배열 요소는 정수이므로 2바
이트의 메모리 공간을 차지한다. 또한 ip는 포인터 이므로 4바이트 메모리 공간
을 차지하고 있으며, cp또한 마찬가지이다. 초기화 문장에 의해 ip와 cp모두는
1000번지를 가리킨다. IBM PC가 아닌 경우 이 그림은 맞다. 하지만, IBM PC인
경우, 엄격히 말하면 인텔 호환 마이크로 CPU를 사용하는 PC인 경우, 실제의 메
모리 그림은 아래와 같다.

  그림 7-3 인텔 CPU인 경우 메모리 구조

  인텔은 2바이트 이상의 변수를 위해 메모리 할당하는 경우, 낮은 바이트는 낮
은 메모리 영역에, 높은 바이트는 높은 메모리 영역에 할당한다. 이것을 역워드
형식이라 한다.

  0x1234가 메모리에 할당되는 경우 1000번지에 할당된다고 하자. 0x1234는 2바
이트 정수이므로26), 1000번지와 1001번지에 걸쳐서 저장될 것이다. 12는 1234

  26) 별다른 표시가 없는 한 상수는 2바이트 정수(int)이다. 정수인 경우, 4바이트로의 명시적

    인 변환은 접미어 L을 사용한다. i=1L은 4바이트 정수 1을 i에 할당한다.

의 상위 바이트에 해당한다. 그러므로, 상위 번지인 1001번지에 저장된다. 하위
바이트 34는 하위 번지인 1000번지에 저장된다. 그러므로 메모리의 구조가 그림
처럼 결정된다.

  하지만 왜 ip+1과 cp+1이 같지 않은가? ip와 cp모두 1000임에는 틀림없다. 핵
심은 여기에 있다. ip는 정수형 포인터(int *)이므로, ip+1은 다음과 같이 해석한
다.

"ip에서 1번째 떨어진 정수"

그러므로, ip+1은 1001이 아니라, 1002이다. 또한 *(ip+1)도 1002번지의 내용이 아
니라, 1002번지와 1003번지의 내용이다. 또한 인텔 CPU는 역워드를 사용하므로
결과는 0x5678이 맞다.

  cp+1은 다음과 같이 해석된다.

"cp에서 1번째 떨어진 1바이트 정수"

그러므로, cp+1은 1001이며, *(cp+1)은 0x12가 맞다. 이것이 왜 (char *)의 명시
적인 형 변환이 필요한지에 대한 해답이다. 일반적으로 포인터 ip의 증감 n의 의
미는 다음과 같다.

ip+n ㅟ ip+sizeof(*ip)*n

그러므로 ip가 long *라면, ip+2는 ip를 8증가시킨다.

  이제 동적 메모리 할당에 대해서 알아보자. 아래의 프로그램 소스는 어디가
잘못인가? 물론 문법적인 에러는 없다. 논리적인 에러를 찾아 보라.

#include <stdio.h>

#include <alloc.h>

void main() {

    int *ip;

    *ip=10;

    printf("%d\n",*ip);

}//main

  위 프로그램을 도스(DOS) 환경에서 실행한다면, 실행될지도 모른다. 하지만
Win95환경에서 실행한다면, 일반 보호 에러(general protection error)를 실행
하고 프로그램은 중단될 것이다.

  핵심은 여기에 있다. ip가 할당된 것이지 *ip가 할당된 것이 아니다. 즉 할당되
지도 않은 *ip영역에 변수를 대입하는 오류를 범하고 있다. 이것은 분명히 보호
에러이다. 아래 그림을 보라 도대체 ip에는 무엇이 들어있는가?

  그림 7-4 dangling pointer

  ip가 1000번지에 할당되었다고 하자. 처음 ip에 들어 있던 값27)을 ?라고 하

  27) 이것을 쓰레기 값(garbage value)라 한다.

면, *ip=10 은 메모리의 ?의 곳에 10을 대입한다. 이것은 명백한 위법이다. ?의
곳이 이미 실행중인 다른 프로그램이 있는 곳이라면, 어떻게 할 것인가? 위 프로
그램은 간단한 다음 규칙을 위배하고 있다.

"변수는 쓰기 전에 선언해야 한다."

  지금은 *ip를 사용할 것이므로, *ip를 위해 메모리를 할당해 주어야 한다. 이것
은 실행 시간(run time)에 할당되므로, 동적 메모리 할당이라 한다. 일반적인 변
수 선언문 int i 같은 것은 컴파일 시간(compile time)에 변수의 메모리 위치가
결정되므로 정적 메모리 할당(static memory allocation)이라 한다28). 그러므로

  28) 변수나 함수의 주소를 결정하는 것을 바인딩(binding)이라 한다. 동적 메모리 할당은

    바인딩이 실행 시간에 일어나므로, 늦은 바인딩(late binding)이라 한다. 늦은 바인딩이 아

    닌 것을 빠른 바인딩(early binding)이라 한다.

위의 소스는 아래와 같이 고쳐져야 한다.

#include <stdio.h>

#include <alloc.h>

void main() {

    int *ip;

    ip=(int *)malloc(2);

    *ip=10;

    printf("%d\n",*ip);

    free(ip);

}//main

malloc(2)는 2바이트의 사용 가능한 메모리를 할당하여 시작 주소를 리턴한다.
그러므로 ?의 곳은 할당된 곳이므로, 이제는 불법이 아니다. malloc()이 메모리의
어디를 할당할 것인가는 염려하지 않아도 된다. 운영체제의 의해 알맞은 메모리
장소가 결정된다. 또한 ip를 위해 (int *)의 형 변환이 반드시 필요하다. 프로그램
이 종료하기 전 동적으로 할당된 메모리는 반드시 해제해야 한다. 그렇지 않으
면, 이 부분이 컴퓨터 전원을 내릴 때까지 계속 남게되어 못쓰는 메모리 영역29)
이 점점 쌓여갈 것이다.

  29) 메모리 누수(memory leak)라 한다.

  이러한 동적 메모리 할당은 빈번히 사용됨에도 불구하고 함수인 것은 못마땅
하다. C++에서는 동적 메모리 할당이 함수에서 연산자로 승격되었다30). C++

  30) 연산자는 함수보다 훨씬 유동적이다. 먼저 함수 호출의 오버헤드가 없다. 또한 매번의

    형 변환이 필요 없으며, 연산자의 문법을 잘 정의하면 - C++에서 new가 그러하듯 - 훨씬

    효과적이다. 또한 C++의 연산자 오버로딩도 지원된다.

에서는 위의 소스를 다음과 같이 new/delete를 사용하여 구현 가능하다.

#include <stdio.h>

void main() {

    int *ip;

    ip=new int;

    *ip=10;

    printf("%d\n",*ip);

    delete ip;

}//main

  alloc.h가 없어졌다는데 주목하라. 또한, new에 더 이상의 형 변환이 필요하지
않다. new int 는 int만큼의 메모리 - 2바이트 - 를 할당하여 시작 주소를 리턴
한다. 그러므로 ip=new int 는 타당한 메모리 할당 문장이다. 메모리 해제는
delete ip 처럼 사용한다. new의 문법은 다음과 같다.

[::]new <type>[(초기값)]

  new앞에 범위 해결사 ::는 옵션(option)이다. 후에 오버로딩된 new와 구분하기
위해서 ::new 처럼 사용할 필요가 생긴다. 이것은 오버로딩된 new를 사용한다는
것이 아니라, 원래의 전역 new를 사용한다는 것을 보장해 준다. new 뒤에 적는
type은 생략해서는 안 된다. new type은 type만큼의 메모리를 할당해서 시작 주
소를 리턴한다. 아래의 문장은 4바이트를 할당한다.

    long *lp;

    lp=new long;

  delete는 할당된 크기에 상관없이,

delete ip;

라고 한다. 메모리 할당 후 대부분은 초기화를 필요로 한다. 그래서 new는 메모
리 할당과 동시에 초기화를 하는 것을 허락한다. 물론 초기화는 생략될 수 있다.
위의 소스는 new의 초기화 문장을 사용하여 다음과 같이 고칠 수 있다.

#include <stdio.h>

void main() {

    int *ip;

    ip=new int(10);

    printf("%d\n",*ip);

    delete ip;

}//main

  그렇다면 10개의 int형 변수를 할당하기 위해서는 어떻게 할 것인가? new는
이러한 여러 개의 기본형을 할당하기 위해서 다음과 같은 문법을 지원한다. 이것
은 new가 지원하는 두 번째 문법이다.

[::]new type[표현식]

  표현식을 둘러싼 브래킷(bracket; [ or ] )은 선택 사항을 나타내는 심벌이 아
니라, 문법 심벌이다. 즉 생략해서는 안 된다. 아래의 문장은 정수 3개를 저장하
기 위해서 6바이트의 메모리를 할당한다.

new int[3]

그러므로 아래의 문장에서 ip는 6바이트의 시작주소를 가리킨다.

    int *ip;

    ip=new int[3];//이것은 정수 3개를 할당한다.

  이것을 2바이트를 할당해서 3으로 초기화하는 아래 문장과 혼돈하지 말라.

    int *ip;

    ip=new int(3);//이것은 정수 1개를 할당한다.

  기본 형 여러 개로 메모리를 할당한 경우 delete의 문법 또한 다르다. new
int[3]처럼 할당된 메모리는 delete[] 로 해제한다. 그러므로, 위의 6바이트 할당
예제에서 메모리 해제는

    int *ip;

    ip=new int[3];

    ...

    delete[] ip;

처럼 한다. 아래의 예제를 참고하라.

#include <stdio.h>

void main() {

    int *ip;

    ip=new int[3];

    *ip=1; *(ip+1)=2; *(ip+2)=3;

    printf("%d,%d,%d\n",*ip, *(ip+1), *(ip+2));

    delete[] ip;

}//main

  불행히도 기본 형 여러 개로 할당하면서 초기화하는 다음과 같은 문장은 허락
되지 않는다.

ip=new int[3](1,2,3);

  하지만, 후에 배울 연산자 오버로딩을 이용하면, 아래와 같은 문장은 허락된다.
물론 new는 연산자 오버로딩 되어야 한다.

ip=new(1,2,3) int[3];31)

  31) 이 문장이 문법적으로 맞다는 의미가 아니다. 이렇게 해도 되도록 new를 오버로드해야

    한다.

  new가 오버로딩 되었다면, 원래의 new를 호출하기 위해서는

ip=::new int[3];

처럼 사용해야 한다. 여러분은 내용 연산자(contents-of operator) []에 대해 알고
있을 것이다. *(ip+n)은 ip[n]과 동일하다32). 그러므로, 위의 소스는 아래와 같

  32) 물론 ip는 포인터 변수라야 하며, n은 정수형이다.

이 고쳐 쓸 수 있으며, 이것이 일반적이다.

#include <stdio.h>

void main() {

    int *ip;

    ip=new int[3];

    ip[0]=1; ip[1]=2; ip[2]=3;

    printf("%d,%d,%d\n",ip[0], ip[1], ip[2]);

    delete[] ip;

}//main

  new를 사용하여 2차원처럼 사용할 수 있는 메모리를 할당하는 것은 놀라운 일
이 아니다. 아래의 예제를 참고하라. 2차원 배열 a[][]에서 a는 포인터의 포인터
로 취급되므로, 2차원처럼 사용하는 메모리를 할당하기 위해서는 포인터의 포인
터 변수 선언이 필요하다.

#include <iostream.h>

void display(long double **);

void de_allocate(long double **);

int m = 3;                               // THE NUMBER OF ROWS.

int n = 5;                               // THE NUMBER OF COLUMNS.

int main(void) {

   long double **data;

   data = new long double*[m];        // STEP 1: SET UP THE ROWS.

   for (int j = 0; j < m; j++)

       data[j] = new long double[n];  // STEP 2: SET UP THE COLUMNS

   for (int i = 0; i < m; i++)

      for (int j = 0; j < n; j++)

          data[i][j] = i + j;            // ARBITRARY INITIALIZATION

   display(data);

   de_allocate(data);

   return 0;

}

void display(long double **data) {

   for (int i = 0; i < m; i++) {

        for (int j = 0; j < n; j++)

             cout << data[i][j] << " ";

       cout << "\n" << endl;

       }

}

void de_allocate(long double **data) {

   for (int i = 0; i < m;  i++)

       delete[] data[i];                 // STEP 1: DELETE THE COLUMNS

   delete[] data;                        // STEP 2: DELETE THE ROWS

}

  delete

  delete는 아래의 두 가지 문법이 있다.

[::]delete <pointer>;

[::]delete[] <pointer>;

  또한 배열 - 포인터의 포인터 - 을 해제하는 경우 다음과 같은 문법을 사용한
다.

[::]delete <array-name>[];

  할당이 ip=new int 처럼 되었다면, delete ip 를 사용한다. 할당이 ip=new
int[3] 처럼 사용되었다면, 반드시 delete[] ip 를 사용한다. ip가 포인터의 포인터
로 선언되었을 때, 포인터의 포인터에 해당하는 값, 즉 배열의 두 번째 인덱스에
해당하는 메모리를 해제할 때는 delete[] ip[0]의 형식으로 사용한다.

  typeid33)

  33) 비록 typeid가 ANSI에서 지정한 표준 키워드임에도 불구하고, 표준이 정해지기 전에 시

    장에 나온, 어떤 C++컴파일러에서는 typeid를 지원하지 않는다. 독자들이 사용하고 있는 컴

    파일러의 도움말을 참조하라.

  C++의 typeid는 실행시간에 표현식이나 형의 형정보(type information)를 구할
수 있다. typeid는 typeinfo라는 클래스의 참조를 리턴한다. 그러므로 typeinfo.h를
반드시 포함하여야 한다.

  typeinfo에는 또한 typeid의 두 가지 멤버 함수를 포함한다. 하나는 name()이
며, 하나는 before()이다. name은 형 이름을 문자열 포인터로 리턴한다. before는
파라미터에 지정된 형의 이름이 사전순(lexical order)으로 뒤에 위치하는지 검사
한다. 아래의 예제와 출력결과를 참조하라.

#include <iostream.h>

#include <typeinfo.h>

class A { };

class B : A { };

char *true="true";

char *false="false";

void main() {

    char C;

    float X;

    // USE THE typeinfo::operator==()TO MAKE COMPARISON

    if (typeid( C ) == typeid( X ))

        cout << "C and X are the same type." << endl;

    else

        cout << "C and X are NOT the same type." << endl;

    // USE true AND false LITERALS TO MAKE COMPARISON

    cout << typeid(int).name();

    cout << " before " << typeid(double).name() << ": " <<

        (typeid(int).before(typeid(double)) ? true : false) << endl;

        //           ^compare lexical order

    cout << typeid(double).name();

    cout << " before " << typeid(int).name() << ": " <<

        (typeid(double).before(typeid(int)) ? true : false) << endl;

    cout << typeid(A).name();

    cout << " before " << typeid(B).name() << ": " <<

        (typeid(A).before(typeid(B)) ? true : false) << endl;

}//main

  출력결과는

C and X are NOT the same type.

int before double: false

double before int: true

A before B: true

이다. int 와 double은 사전에서 double이 먼저다. A는 B보다 사전에서 먼저 나
타난다.

  dynamic_cast

  클래스에 대해 모르는 독자들이나, 클래스의 상속성(inheritance)에 대해 확실
히 개념이 잡히지 않은 독자는 이 부분을 건너 뛰라. 상속성에 대한 개념 없이
dynamic_cast를 이해하는 것은 불가능하다.

  여러분은 dynamic_cast를 이해하기 전에 서브타입의 원리(subtype
principle)에 대해 이해할 필요가 있다. 아래와 같은 클래스 계층 구조를 생각해
보자.

  그림 7-5 subtype principle

  클래스 B의 포인터 변수 bp를 고려해서, 다음과 같은 문장이 가능한가에 대해
이야기해 보자. 먼저 위의 그림에 해당하는 소스는 아래와 같다.

class A { void f(){} };

class B : public A { void f(){} };

class C : public B { void f(){} };

class D : public C { void f(){} };

void main() {

    B *bp;

    A a;

    B b;

    C c;

    D d;

    bp=&b;//명시적인 형 변환이 없어도 가능하다.

}//main

  bp=&b 는 타당한 문장이다. bp는 클래스 B형의 객체 b의 시작 주소를 가리킨
다. 그러므로, bp->f()는 타당한 함수 호출이며, b의 멤버 함수 f()가 호출된다.
그렇다면, 다음 문장은 가능한가?

bp=&a;

불가능하다. 아래처럼 형 변환을 해주면 어떤가?

bp=(B *)&a;

역시 불가능하다. 자식이 부모의 포인터를 가지는 것은 불가능하다. 왜 그런가를
이해하기 전에 다음 문장을 고려해 보자.

bp=(B *)&c;

bp=(B *)&d;

  위 두 문장은 모두 가능하며, bp->f()는 각각 클래스 B의 멤버 함수 f()를 호
출한다. c의 멤버 함수 f()와 d의 멤버 함수 f()가 호출되게 하려면, f()를 가상 함
수(virtual function)로 만들어야 한다. 이 부분은 '가상 함수'에 관한 부분을 참고
하라. 왜 하위 클래스 - subtype34) - 의 포인터를 가지는 것만이 허용되는가?

  34) 상위 클래스를 supertype이라고 한다며, 상속 받은 하위 클래스는 subtype이 된다. 물론

    super class/sub class, base class/derived class라는 용어도 사용한다.

또 어떻게 하위 클래스의 멤버 함수를 호출하는가? 는 중요한 주제이다.

  상위 클래스의 포인터 변수를 이용해 하위 클래스의 멤버를 접근하는 것은 자
연스럽다. 하지만 하위 클래스의 포인터를 이용해 상위 클래스의 멤버 함수를 접
근하는 것은 부자연스럽다. 어떻게 자식(child)이 부모(parent)의 소지품을 건드릴
수 있는가? bp가 B *이므로 등호의 오른쪽에 &b가 올 수 있는 것은 당연하다.
여기에 서브타입의 원리의 핵심이 있다. 원리는 다음과 같다.

"supertype이 올 수 있는 자리에는 항상 subtype이 올 수 있다."

  등호의 오른쪽에 B *가 올 수 있으므로, B의 서브 타입에 해당하는 C *와 D
*는 올 수 있다. 하지만, B *의 수퍼 타입에 해당하는 A *는 올 수 없다. 이 원
리는 가상함수와 함께 C++프로그래머라면 반드시 기억하고 있어야 한다.

  하지만, 이 반대의 형 변환을 지원하는 컴파일러도 있다. 이러한 형 변환은 실
행시에 일어나므로, 동적 형 변환(dynamic casting)이라 한다. 동적 형 변환을
위한 키워드는 dynamic_cast 이다. 단 한가지 제약이 있다. 형 변환이 되는 클래
스는 반드시 다형성 클래스(polymorphic class)35)라야 한다. 즉 최소한 1개

  35) 1개 이상의 가상함수를 가지는 클래스를 다형성 클래스라 한다. 1개 이상의 순수 가상

    함수를 가지는 클래스는 객체를 만들 수 없으므로, 추상 클래스(abstract class)라 한다.

    추상 클래스는 다형성 클래스에 속한다. 다형성 클래스의 가상함수에 관한 호출은 실행 시

    에 결정되므로 다형(polymorphic)이란 말은 적절하다.

이상의 가상 함수를 멤버로 가져야 한다. 생각해 보라. 가상함수를 가지지 않는
다면, 상위 클래스로의 형 변환이 왜 필요한가?

  아래의 예제는 다음과 같은 클래스 계층 구조에서의 형 변환을 보여준다.

  그림 7-6 클래스 계층 구조

  dynamic_cast를 사용하면, Base1 *를 Derived *로 형 변환하는 것이 가능하다.
또한 Base1 *를 Base2 *로 형 변환하는 것도 가능하다. 단 Base1에는 반드시 1
개 이상의 가상함수가 존재해야 한다. 문법은 다음과 같다.

dynamic_cast<converted_class_pointer>(original_class_pointer)

위 문장은 original_class_pointer를 converted_class_pointer로 변환한다. 물론
converted_class pointer는 original_class_pointer의 서브 클래스가 될 수 있다. 아
래의 예제를 참고하라.

//This program must be compiled with the -RT (Generate RTTI) option.

//Run Time Type Information isn't supported

//  by some compiler like Borland C++ 3.1

#include <iostream.h>

#include <typeinfo.h>

class Base1 {

   // In order for the RTTI mechanism to function correctly,

   // a base class must be polymorphic.

   virtual void f(void) { /* A virtual function makes the class

                             polymorphic */ }

};//class Base1

class Base2 { };

class Derived : public Base1, public Base2 { };

int main(void) {

      Derived d, *pd;

      Base1 *b1 = &d;

      // Perform a downcast from a Base1 to a Derived.

      if ((pd = dynamic_cast<Derived *>(b1)) != 0) {

           cout << "The resulting pointer is of type "

                << typeid(pd).name() << endl;

      }//if

      // Attempt cast across the hierarchy.  That is, cast from

      // the first base to the most derived class and then back

      // to another accessible base.

      Base2 *b2;

      if ((b2 = dynamic_cast<Base2 *>(b1)) != 0) {

          cout << "The resulting pointer is of type "

               << typeid(b2).name() << endl;

      }//if

      return 0;

}//main

/* 결과:

   The resulting pointer is of type Derived *

   The resulting pointer is of type Base2 *         */

  이 외에도 실행시 형 정보(run time type information: RTTI)에 관한 키워
드로 static_cast, const_cast, reinterpret_cast와 typesafe_downcast가 있다.
이러한 키워드는 몇몇 컴파일러에서는 지원되지 않으며, 또한 고 난이도의 시스
템 프로그램을 코딩하지 않는 이상 사용될 일은 잘 없다. 하지만, 궁금한 독자들
은 자신이 사용하는 컴파일러의 도움말 시스템을 이용하여, 위에서 열거한 키워
드에 대해서 각자 찾아보기 바란다. 필자가 독자들에게 충고하는 마지막 조언은
다음과 같다.

"subtype의 원리를 되도록이면 위반하지 말라. 즉 dynamic_cast는
되도록이면 사용하지 말라."

  콤마(Comma)

  가끔식 사용되기는 하지만, 콤마 연산자는 유용하다. 콤마 연산자의 문법은 다
음과 같다.

표현식[,표현식][...]

  표현식은 콤마에 의해 계속 연결될 수 있으며, 콤마에 의해 연결된 표현식은
하나의 문장이다. 그리고 문장의 값은 마지막 표현식의 값이 된다. 그러므로 다
음 문장의 값은 3이다.

1,2,3

  위의 문장이 타당한 표현식임에 유의하라. 아래의 소스는 3을 i에 할당한다.

i=(1,2,3);

  콤마 연산자는 우선 순위(priority)가 가장 낮다. 그러므로 i=1,2,3 이라는 문장
은 (i=1),2,3으로 해석되므로, i의 값은 1로 초기화되며, 문장의 값은 3이다. 만약
j=((i=1),2,3)으로 쓰여졌다면, i에 1을 할당하며, 문장의 값 3을 j에 할당한다. 그
러므로 다음 문장

i=1,j=2,k=3;

은 i에 1을 할당하고, j에 2를 할당하고, k에 3을 할당하며, 전체 문장의 값은 마
지막에 실행된 문장의 값인 3이 된다. 콤마 연산자로 연결된 표현식은 왼쪽에서
오른쪽으로 평가된다. 또한 문장의 값은 마지막에 실행된 표현식의 값이다.

  위 문장은

i=1; j=2; k=3;

과는 다르다. 이것은 3개의 문장이며, 문장 각각이 1,2,3이라는 값을 가지지만, 전
체 문장이 값을 가지는 것은 아니다. 문법에서도 언급했듯이, 콤마 연산자로 연
결할 수 있는 것은 표현식만이다. 아래의 소스의 결과에 주목하라.

#include <stdio.h>

void main() {

    int i;

    printf("%d\n",(1,2,3));//값은 3이다.

    //       3

    i=(3,2,1);//마지막 값인 1이 i에 할당된다.

    printf("%d\n",i);

    //       1

}//main

  표현식만으로 연결되어야 하므로, 다음 문장은 에러이다.

i=1, int j, k=2;

두 번째 문장은 표현식이 아니므로, 즉 값을 가지지 않으므로, 콤마로 연결할 수
없다.

  콤마 연산자는 반드시 한 문장으로 표현해야 하지만, 2개 이상의 문장이 필요
한 경우 많이 사용된다. 사실 콤마는 이 때 밖에 사용할 일이 없다. i를 1부터 10
까지 변화시키면서, j를 5부터, 50까지 5의 배수로 크기를 변화시켜야 한다고 생
각해 보자. 여러분은 for문을 사용하여 소스를 다음과 같이 작성할 수 있다.

j=5;

for (i=1;i<=10;++i) {

    ...

    j+=5;

}//for

  이러한 문장이 가능은 하지만, 인덱스 변수가 2개라는 사실을 표현하는데는 부
족하다. 콤마 연산자를 사용하여 다음과 같이 소스를 수정할 수 있다.

for (i=1,j=5;i<=10;++i,j+=5)

    ...

  마지막으로 아래 프로그램의 결과를 예측해 보라.

#include <stdio.h>

void f(int i,int j) {

    printf("%d,%d\n",i,j);

}//f

void main() {

    int i,j,k;

    f((i=1,j=2),k=3);

}//main

  결과는 2,3이 출력된다. 당신의 결과와 일치하는가? 이것은 콤마 연산자를 사
용하여, 초기화와 함수의 파라미터 전달을 동시에 한 경우의 적절한 예이다. 마
지막 조언은 다음과 같다.

"for문에서의 어쩔 수 없는 경우를 제외하고는, 되도록이면 콤마 연산자를
사용하지 말라. 즉 위와 같은 함수의 호출에 콤마 연산자를 사용하는 것은
좋지 않다."

  조건(Conditional)

  조건 연산자는 C에서 유일한 삼항 연산자(ternary operator)이다. 삼항 연산
자라는 말에 유의하라. 이항 연산자(binary operator)에서 피연산자(operand) 2개
가 모두 명시되어야 하듯이, 비록 필요 없는 경우가 생기더라도 조건 연산자의
피연산자 3개는 모두 명시되어야 한다. 조건 연산자의 문법은 다음과 같다.

표현식0 ? 표현식1 : 표현식2

  위 문장36)은 표현식0의 값이 0이 아니면, 즉 참(true)이면 표현식1의 값으

  36) 문장과 표현식을 혼용하여 사용하고 있지만, 독자들은 명확히 구분하기 바란다. 사실

    문장의 값이 수(number)이므로, 표현식이라는 말이 정확하다. 하지만, 문장의 특성을 강조

    하기 위해서, 표현식인 경우도 문맥에 따라 문장이라는 용어를 사용하고 있다.

로, 아니면 표현식2의 값으로 결정된다. 그러므로 아래 문장의 값은 2이다.

0 ? 1 : 2

  if문으로 표현된 아래 문장은

    if (i>j)

        m=i;

    else

        m=j;

다음의 조건 연산자로 표현된 문장과 동일하다.

m=(i>j)?i:j;

즉, i와 j중 큰 값을 m에 할당한다. if문의 표현이

    if (i>j)

        m=i;

라고 해서, 대등한 조건 연산자 표현으로

m=(i>j)?i: ;

라고 해서는 안된다. 조건 연산자가 삼항 연산자이므로, 피연산자 어느 것도 생
략해서는 안된다. 아래의 연산자는 무엇을 의미하는가?

m=(i>j)?i:(j>k)?j:k

대등한 if문은 다음과 같다.

if (i>j)

    m=i;

else {

    if (j>k)

        m=j;

    else

        m=k;

}

이다. 그러므로 아래 문장의 결과는 5이다.

#include <stdio.h>

#include <iostream.h>

void main() {

    int m,i=2,j=5,k=3;

    m=(i>j)?i:(j>k)?j:k;

    cout << m <<endl;

}//main

  여러분은 조건 연산자를 이용하여 max혹은 min이라는 매크로 함수(macro
function)을 다음과 같이 만들 수 있을 것이다.

    #define MAX(a,b) ((a)>(b)?(a):(b))37)

  37) 매크로 함수의 파라미터를 괄호로 감싸야 함에 유의하라. 그렇게 하지 않는다면, 연산

    자 우선 순위에 의해 심각한 문제가 발생할 수 있다. 이것은 '매크로 함수'에서 상세히 다

    룬다.

    #define MIN(a,b) ((a)<(b)?(a):(b))

  물론 C++에서 이러한 매크로 함수는 권장되는 사항이 아니다. C++에서는 위의
함수 대신 다음과 같은 인라인 함수(inline function)를 작성할 것이다.

inline int Max(int a,int b) {

    return a>b?a:b;

}//Max

  위의 Max함수는 정수형의 Max에 대해서만 동작한다. 모든 형에 대해 동작하
는 Max함수는 템플릿(template)을 이용하여 구현 가능하다. 아래의 소스는 아직
몰라도 된다. 후에 일반 함수(generic function)에서 자세히 다룰 것이다.

template<class T> inline T Max(T a,T b) {

    return a>b?a:b;

}//Max

  논리(Logical)

&&	논리곱(logical AND) 연산자

||	논리합(logical OR) 연산자

!	단항 논리부정(logical NOT) 연산자

  먼저, 주의해야 할 사항이 있다. C에서의 논리 연산자와 수학에서의 논리 연산
자는 의미가 다르다. 수학에서는 논리 연산자의 표현식이 논리 표현식38)이지

  38) 표현식의 결과가 참(true: 1) 혹은 거짓(false: 0)인 표현식

만, C에서는 수치 표현식이다. 즉 논리 연산의 결과는 참/거짓이 아니라 수
(number)이다. 이것을 제외한 다른 차이점은 없다. C는 0을 거짓으로 간주하며,
0이 아닌 숫자39)를 참으로 간주한다. 0을 거짓으로 보고, 1을 참으로 본다면,

  39) 숫자가 0이 아니면, 비트열(bit sequence) 중 최소한 1비트는 1이다. 그러므로 C언어에

    서 참/거짓의 검사는 모든 비트가 0인지, 그렇지 않은지를 검사한다.

진리표는 아래와 같다.

값

&&

||

0

0

1

1

0

1

0

1

0

0

0

1

0

1

1

1

  표 7-2 논리 연산자의 진리표

값

!

0

1

1

0

  표 7-3 논리 부정의 진리표

  0은 거짓이지만, 0이 아니면 항상 참이라는 것에 주목하라. 1은 참이다. 100도
참이며, -1도 참이다. 그러므로 !0은 참이다. !1, !100, !-1은 참을 부정하는 문장
으로 간주하므로, 거짓(0)이다. 그러므로 printf("%d\n",!-1)은 0을 출력한다.

  아래 프로그램의 결과는 얼마일까?

int i=2,j=3,k=4;

if (i<j && j<=k)

    printf("logical operator\n");

  문자열 "logical operator"는 출력된다. 왜 문자열이 출력되는가? 아래와 같은
대답은 맞는가? 대답이 맞다고 생각하는 사람은 자신의 생각이 틀렸다는 것을
꼭 확인하라.

"i<j는 참이다. j<=k는 참이다. '참 AND 참'은 참이므로 if의 조건이 만족된다.
그러므로, printf()가 실행된다."

  위의 대답이 틀린 것은 아니지만, 좀 더 정확한 대답은 다음과 같다.

"i<j는 관계 비교가 참이므로 1이다. j<=k역시 1이다. '1 AND 1'은 1이므로
printf()가 실행된다."

즉 if문이 실행된 이유는 괄호 안의 조건이 참이기 때문이 아니라, 괄호 안의 표
현식이 1이기 때문이다. C에서 참/거짓이란 값은 존재하지 않는다. 0이면 조건
비교를 거짓으로 간주하고, 0이 아니면 참으로 간주한다. 이 개념을 확실히 알고
있다면, 참/거짓이란 말을 사용해도 좋다.

  그러므로 아래 프로그램의 출력결과는 1이다.

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    printf("%d\n",i<j && j<=k);

}//main

  말하려는 핵심(key)은 다음과 같다.

"논리 연산자와 관계 연산자의 결과는 0아니면 1이다. 즉 표현식이다."

  논리 연산자를 다룰 때, 반드시 알고 있어야 하는 사항은 '짧은 평가(short
circuit)'이다. 위의 예에서 if의 조건이 논리합(OR)이라면 결과는 얼마인가?

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    if (i<j || j<=k)//짧은 평가에 의해, j<=k는 평가하지 않는다.

        printf("short circuit\n");

}//main

  물론 "short circuit"이란 문자열은 출력된다. 둘 다 참(1)이므로, 논리합의 결과
역시 참이다. 핵심은 여기에 있다. j<=k를 평가해야 하는가? 사실 이 평가는 불
필요하다. i<j가 참이라면, 논리합의 특성에 의해 두 번째 표현의 참/거짓에 관계
없이 항상 참이다. 실제로 컴파일러는 j<=k를 평가하지 않는다. 이것을 짧은 평
가라 한다. 물론 논리곱(AND)에 대해서도 짧은 평가는 성립한다. 짧은 평가에
관한 조언은 다음과 같다.

"연속된 조건을 명시할 때, 빠르게 참/거짓이 결정될 것 같은 문장들을
앞쪽(왼쪽)에 써라. 이것은 짧은 평가에 의해 조금의 속도 향상을 꾀할 수
있다."

  논리 부정(NOT)은 전체 논리식을 부정한다. 사실상 수학에서와 같이, 논리 부
정은 다음 연산자를 서로 교체(toggle)한다.

&&  ↔  ||

>  ↔  <=

>=  ↔  <

==  ↔  !=

그러므로 다음 문장은

!(i>j && j!=k)

아래 문장과 같다.

(i<=j || j==k)

  그렇다면 언제 논리 부정 연산자를 사용할 것인가? 문맥에 맞게 논리 부정을
사용하라. 독자들의 의도가 'i가 j보다 크지 않은지 비교한다'면 i<=j보다 !(i>j)가
적당할 것이다. 물론 차이는 없다. 하지만, 의도대로의 코딩은 프로그램을 읽기
쉽게(improve readability) 한다.

  아래에 논리 연산자의 전체적인 예를 들었다. 프로그램의 결과는 적지 않는다.
꼭 실행해서 결과를 확인해 보라.

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    printf("%d\n",i && j);

    printf("%d\n",!i && !j);

    printf("%d\n",i>j || j==k);

    printf("%d\n",!(i>j));

    printf("%d\n",!(!(i<j)));

    printf("%d\n",!(j!=k));

}//main40)

  40) 1 0 0 1 1 0이 출력된다.

  종종 무한 루프(infinite loop)를 만들어야 하는 경우가 생긴다. 논리 연산의
특징을 이용해서 다음과 같은 무한 루프를 만드는 것이 가능하다.

while(1) { ... }

do { ... } while(1);

for(;1;) { ... }41)

  41) for(;;) { ... }역시 무한 루프이다. for문에서 생략된 비교 문장은 참을 의미하며, 이것이

    일반적이다.

  while(-1) { ... } 역시 무한 루프이다. 하지만, 관례(convention)상 무한 루프를
만드는 표현식은 1을 사용한다.

  후위표기(Postfix)

  C에서 연산자처럼 느껴지지 않는 몇몇 연산자들은 후위표기법(postfix
notation)42)을 사용한다. 아래의 연산자들은 후위 표기법으로 표현하며, 일반

  42) 일반적으로 연산자(operator)는 피 연산자(operand)들의 사이에 위치한다. i + j는 일반적

    이다. 하지만, 피 연산자가 연산자보다 먼저, 혹은 나중에 위치하도록 표기할 수도 있다. i

    j +는 전위표기법(prefix notation)이라 한다. + i j는 후위 표기법이라 한다. 어떤 경우 후

    위 표기법 등이 연산을 처리하기에 훨씬 효과적인 경우가 많다. 실제적으로 계산기의 표현

    식 평가는 중위 표기법(infix notation)을 후위 표기법으로 전환(transform)한 다음 평가한

    다. 표기법의 전환은 스택을 사용하여 어렵게 구현 할 수 있다.

적이다. 연산자임에는 분명하지만, 표현식이 아닌 것도 있다. 예를 들면 ()는 함
수 호출 연산자(function call operator)이지만, 결과가 수(number)인 것은 아니다.

()	표현식을 그룹화 하기 위해, 조건 표현식을 독립시키기 위해, 함수 호출

    을 위해, 함수의 파라미터를 지정하기 위해 사용한다. 함수 호출 연산자

    (function call operator)라 한다.

[]	배열의 첨자(subscript)를 가리키기 위해, 포인터가 가리키는 곳의 내용

    을 참조하기 위해 사용한다. 첨자 연산자 혹은 내용 연산자

    (contents-of operator)라 한다.

{}	복합문(compound statement)의 시작과 끝을 나타내기 위해 사용한다.

    복합문은 하나의 문장 취급된다. 블락(block)이라고 한다.

.	구조체(structure), 공용체(union)와 클래스(class)의 멤버를 접근(access)

    하기 위해 사용한다. 멤버 연산자(member operator)라 한다.

->	포인터로 선언된 구조체 등의 멤버를 접근하기 위해 사용한다. 포인터

    멤버 연산자(pointer member operator)라 한다.

  함수 호출 연산자 ()는 함수 호출이라는 특별한 목적이외에도 문법 구조를 이
루는 심벌로 자주 등장한다. 2+3*4는 14이다. 하지만 (2+3)*4는 20이다. 이것은
2+3이라는 것을 그룹화한 것을 의미한다. ()는 연산자의 우선 순위가 가장 높다.

  함수 호출 연산자에 대해 살펴보자. 아래의 소스를 보라. 무엇이 잘못 되었는
가?

#include <stdio.h>

#include <conio.h>

void main() {

    int i;

    printf("Press y key");

    i=getch;//이 부분이 잘못인가?

    if (i=='y')

        printf("You pressed small y key\n");

}//main

  아마도 사용자는 getch43)의 뒤에 괄호 ()를 적는 것을 빠뜨린 것 같다. 하지

  43) getch()는 키보드에서 한 개의 키 입력을 받아, 그 키의 아스키(ASCII) 코드 값을 2바이

    트 정수로 리턴한다. 이것은 표준함수가 아니므로 conio.h에 선언되어 있다.

만, 독자들이 주의해야 할 사항이 바로 여기에 있다. 컴파일 시간 에러가 난 원
인은 다음과 같다.

"getch 뒤에 ()가 없어서 에러가 난 것이 아니다. getch 는 함수의 시작 주소
를 나타내는 함수 포인터(function pointer)이고 i는 정수(integer)이기 때문
에, 형 불일치(type mismatch) 에러가 난 것이다."

  즉 위의 에러 원인은 float형의 변수를 int에 대입하려고 했을 때 발생하는 에
러와 같다. 그렇다면 getch는 무엇인가? 바로 getch의 시작 주소이다. 그러므로
함수 호출이 일어나기 위해서는 함수 호출 연산자를 함수의 시작 주소 뒤에 써
주어야 한다. 수정된 소스는 아래와 같다.

#include <stdio.h>

#include <conio.h>

void main() {

    int i;

    printf("Press y key");

    i=getch();//함수 호출이 일어난다.

    if (i=='y')

        printf("You pressed small y key\n");

}//main

  후에 포인터를 다룰 때, 함수 포인터에 대해서 자세히 다룰 것이다. 지금 이해
가 되지 않는다고 걱정할 필요는 없다.

  []는 배열의 인덱스 연산자로 알고 있지만, 실제로는 내용 연산자
(contents-of operator)이다. 배열의 인덱스 연산자는 배열의 이름이 시작 주소
인 바람에 얻어지는 내용 연산자의 부산물(added goods)이다. C++에서 []는 배열
에 대해서 적용하는 경우보다 포인터에 대해 적용하는 것이 일반적이다. 이 부분
역시 '[] 연산자'에서 자세히 다루므로 넘어 간다. 배열의 인덱스 연산자로 쓰이
는 부분만 간단히 알아보자.

  int a[5]={1,3,5,7,9} 는 크기가 5인 정수형 배열을 선언하여 1부터 시작하는 홀
수로 배열의 각 셀(cell)을 초기화한다. 실제로 할당된 메모리는 10바이트이며, 각
각의 정수는 인덱스(index)를 이용하여 참조한다. 타당한(valid) 인덱스는 0부터
4이다. 배열의 인덱스가 0부터 시작한다는데 주의하라. 파스칼(Pascal)같은 언어
등에서는 1부터 시작하기도 한다.

  이런 상황에서 배열 a의 2번째 요소 3을 출력하기 위해서는

printf("%d",a[1]);

을 사용할 수 있다. 배열의 요소를 참조하기 위해 사용하는 []을 배열의 인덱스
연산자라 한다.

  여러 개의 문장이 모여 하나의 문장을 이룰 때 이를 복합문(compound
statement)이라 한다. {} 는 복합문, 즉 블락 구조(block structure)를 만들기
위해 사용한다. 복합문이라는 단어가 의미하듯이 복합문이 하나의 문장이라는 것
에 주의하라.

  우리가 알고 있는 if문의 문법은 다음과 같다.

if (표현식1)

    문장1;

[else if (표현식2)

    문장2;][...]

[else

    문장3;]

  이것은 if 뒤의 괄호 안의 표현식이 참이면(0이 아니면), 연속하는 문장 1개를
실행함을 의미한다. 예를 들면, 아래의 소스는

int i=2,j=3;

if (i<j)

    printf("i is greater than j.\n");

  i<j 가 참이므로, printf()를 실행한다. 만약 괄호 안의 조건이 참이 될 때, 실행
할 문장이 2문장 이상이라면, 어떻게 할 것인가?

if (i<j)

    printf("i is greater than j.\n");

    printf("it\'s joke.\n");

위와 같은 단순한 들여 쓰기에 의해, 두개의 printf()문장이 실행되는 것은 아니
다. if의 문법에서 보듯이 괄호 안의 조건이 만족된다면, 항상 바로 밑에 있는 문
장 1개를 실행한다. 그러므로 위의 소스는 if의 조건에 상관없이

printf(("it\'s joke.\n");

는 항상 실행한다. 어떻게 이 문제를 해결할 것인가? 바로 복합문 연산자, 즉 2
개의 문장을 하나의 문장으로 만드는 것이다. {}를 사용하여 해결한 소스는 아래
와 같다.

    if (i<j) {

        printf("i is greater than j.\n");

        printf("it\'s joke.\n");

    }

  어떤 책에서는 if의 문법을 설명하기 위해

if (표현식)

    문장;

or

if (표현식) {

    문장;

    ...

}

이라는 두 개의 표현을 병행하고 있다. 틀린 것은 아니지만, 복합문 연산자, 즉
블락 구조를 만드는 { 와 } 의 역할을 제대로 이해하고 있지 못하기 때문이다.
즉, 제어 구조(control structure)44)에서 사용된 - switch 문을 제외하고 -

  44) 제어 구조에는 if, switch, for, while과 do문의 5개가 있으며, 이것이 전부다.

{}는 제어 구조의 문법을 이루는 심벌이 아니라, 블락 구조를 나타내는 심벌이다.

  블락 구조(block structure)는 다음과 같은 특징이 있다.

  (1) 하나의 문장 취급된다.

  (2)-a 블락의 첫 부분에, 변수 선언을 가질 수 있다.(C언어)

  (2)-b 블락 안에서는 어디나 변수 선언을 가질 수 있다.(C++언어)

  [(3) 겹쳐질(nesting) 수 있다.]

(3)번은 블락이 하나의 문장 취급되므로, 당연한 사실이다. 이것은 제어 구조가
아니더라도, 블락 안에 얼마든지 블락을 만들 수 있음을 의미한다.

#include <stdio.h>

void main() {

    printf("a");

    {

        printf("b");

        {

            printf("c");

        }

    }

}

위 소스는 에러가 아니며, 완전하다. 함수의 정의가

<함수의 헤더> <블락>

으로 이루어져 있음에 주목하라. void main()은 함수의 헤더에 해당하고, { }는
바로 1개의 블락이다. 흔히 함수의 블락은 몸체(body)라고 한다.

  (2)번 성질은 왜 함수의 몸체에 변수 선언이 타당한가에 대한 근거이다.

{

    int i,j;//C에서 변수 선언은 반드시 블락의 시작에 위치한다.

    printf("C specific");

}

위의 소스는 C/C++에서 모두 가능하다. 하지만,

{

    int i,j;

    printf("C specific");

    int k;//C++에서는 블락의 임의의 위치에 변수선언을 가질 수 있다.

    printf("C++ specific");

}

는 C++에서만 가능하다.

  여러분이 블락을 이해하면서 이해해야할 또 한가지는 블락 안에서 선언된 변
수에 관한 것이다. 블락 안에서 선언된 변수는 블락 안에서만 사용할 수 있다.
이것을 변수가 블락 범위(block scope)45)를 가진다고 한다. 블락 범위를 가

  45) 변수의 입장에서 블락 범위를 가진다. 변수를 사용하는 입장에서, 블락 안에서만 변수

    가 보인다. 그래서 로컬 변수는 블락에서만 보인다(block visibility)고 한다. 즉 scope와

    visibility는, 캡슐화(encapsulation)와 추상화(abstraction)처럼 입장의 차이이다.

지는 변수를 지역 변수(local variable)라 한다.

  . 와 -> 는 C에서 구조체/공용체의 멤버 연산자로 사용되었다. 물론 C++에서
클래스의 멤버를 접근하기 위해서 사용된다. 클래스는 구조체의 확장된 개념이므
로 이것은 자연스럽다. 후에 클래스에 대해서는 자세히 다루므로, 구조체에 대해
서만 설명한다.

  구조체는 여러 개의 서로 다른 데이터 형(data type)을 필드(field)로 가지는
데이터 형이다. 다른 언어에서는 이것을 레코드(record)라고 하기도 한다. 클래
스와의 호환성을 위해 필드란 말 대신 멤버란 말을 사용하기로 하자. 구조체에
관한 설명과 구조체를 정의하는 법은 이 책의 뒷부분의 '구조체' 부분을 참고하
기 바란다. 아래의 예제에서 STest라는 구조체는 두개의 멤버를 가지며, 이 멤버
를 참고하기 위해, . 을 사용하고 있다.

#include <stdio.h>

//#include <string.h>

#include <iostream.h>

struct STest {

    int age;

    char name[80];

};//struct STest

void main() {

    struct STest46) s={30,"Seo JinTaek"};

  46) C에서는 구조체의 형 이름은 struct STest이다. 하지만, C++에서는 STest도 형 이름이

    다. 만약 여러분이 C++모드에서 이 프로그램을 컴파일한다면, struct STest를 STest로 대치

    해서 컴파일해도 문제가 없다. 이것은 클래스와의 호환성 때문에 C++에서 개선된 사항이

    다.

    cout << s.age << endl;//STest의 멤버 age를 참고하기 위해 . 을 사용한

                          //다.

    cout << s.name << endl;

}//main

  배열과는 다르게 s는 구조체 자신임 - 이것이 무엇이든 - 을 주의하라. 구조체
의 시작 주소를 얻기 위해서는 &s를 사용해야 한다.

  구조체가 선언되지 않고 구조체를 가리키는 포인터가 선언된 경우 어떻게 할
것인가?

#include <stdio.h>

#include <string.h>

#include <iostream.h>

struct STest {

    int age;

    char name[80];

};//struct STest

void main() {

    struct STest *s;

    s=new STest;//포인터이므로 메모리 할당이 필요하다.

    (*s).age=30;

    strcpy((*s).name,"seojt");//(*s).name="seojt"는 왜 안되는가?

    cout << (*s).age << endl;

    cout << (*s).name << endl;

    delete s;//new로 할당한 메모리는 반드시 delete한다.

}//main

독자들은 s가 구조체가 아니라, s가 가리키는 내용, 즉 *s가 구조체라는 것을 알
고 있을 것이다. 그러므로 구조체 *s의 멤버를 접근하기 위해 . 을 사용하여,
(*s).age 처럼 쓴다. 연산자의 우선 순위 때문에 괄호가 반드시 필요하다. 여기에
핵심이 있다.

  (*s).age는 s->age와 동일하다. 즉 -> 는 구조체 포인터 변수에서 쉽게 멤버
를 접근하기 위해 사용되는 연산자이다. 이것은 구조체가 포인터로서 사용될 일
이 많기 때문에 효율적인 코딩 - 타이핑을 줄이기 위해 - 을 위해서 만들어진
연산자이다. 수정된 소스는 아래와 같다.

#include <stdio.h>

#include <string.h>

#include <iostream.h>

struct STest {

    int age;

    char name[80];

};//struct STest

void main() {

    struct STest *s;

    s=new STest;

    s->age=30;

    strcpy(s->name,"seojt");

    cout << s->age << endl;

    cout << s->name << endl;

    delete s;

}//main

  전처리(Preprocessor)

#	스트링화47) 연산자

  47) 적당한 영문을 찾지 못해 쓰지 못한다. enstring operator라는 말이 적당한 것 같지만,

    enstring이라는 말은 사전에 없다.

##	토큰 연결(token concatenation) 연산자

  #는 파운드 기호(pound sign)이라고 읽고, ##는 더블 파운드 기호(double
pound signs)라고 읽는다. 일반적으로 겼(sharp)라고 읽으므로, 이렇게 읽어도 -
한국에서 - 무방하다.

  독자들은 처리(processing)란 말이 컴파일러가 기계어 코드를 생성하는 과정을
의미한다는 것을 알고 있을 것이다. 그래서 컴파일 전에 사용되는 명령문을 전처
리 명령문(preprocessing operator) 혹은 컴파일러 지시자(compiler
directive)라 한다. 컴파일하기 전에 어떤 일을 지시하는 것이다. 위의 두 가지
연산자는 전처리 명령문에 사용되기 때문에, 전처리 연산자로 구분된다.

  #은 큰따옴표("")가 없는 문자 순서(string sequence)를 문자열로 만든다.

#define stringit(x) #x

라고 선언되었다면, 프로그램 소스에서

stringit(Sin JinTaek)

라고 쓰면, 컴파일 전에 "Sin JinTaek"라고 치환된다.

  ##는 두 개의 토큰(token)48)을 컴파일 전에 연결하는 연산자이다.

  48) 컴파일러가 기계어 코드를 만들기 위해 처리하는 기본 단위를 토큰이라 한다. void

    main ( )에서 토큰은 4개이다.

#define tokencat(x,y) x##y

라고 선언되었다면,

tokencat(i,j)

라고 쓰면, 컴파일 전에 ij 로 치환된다. 아래의 소스를 참고하라.

#include <iostream.h>

//#define charit(x) #@x//이 연산자 #@는 각자가 사용하는 컴파일러의 도움말

                       //을 참고하라.(궁금하다면)

#define stringit(x) #x

#define tokencat(x,y) x##y

void main(void)

{

    int i=1,j=2,ij=3;

    cout << stringit(hello) << '\n';

    cout << tokencat(i,j) << '\n';

}

/* hello

   3    */

  참조/역참조(Reference/Dereference)

&	참조 연산자 혹은 주소 연산자(address-of operator)

*	역참조49) 연산자

  49) 어떤 책에서는 재참조 연산자, 간접지정 연산자(indirect operator)라고도 한다.

  &는 앰퍼샌드(ampersand)라고 읽으며, *는 애스트리스크(asterisk)라고 읽는다.
*는 별표(star)라고 읽어도 - 한국에서 - 무방하다.

  &는 데이터가 저장된 곳의 주소를 얻기 위해 사용한다.

int i=2;

라고 선언된 변수 i에 대해, i는 2를 의미한다. 하지만, &i는 실제 값 2가 들어
있는 곳의 메모리의 위치, 즉 주소(address)를 의미한다. i값을 의미하는 것이 아
니라, i값이 있는 곳의 참조(reference)를 의미하므로, 참조 연산자라 한다.

  참조는 주소를 의미하므로, 포인터 변수에만 대입 가능하다.

int j;

에 대해

j=&i;

는 불가능하다. 형이 다른 변수끼리는 대입이 불가능하다. 아래의 소스는 j를 i의
참조 값, 즉 i의 주소로 초기화시킨다.

#include <stdio.h>

void main() {

    int i=2;

    int *j;

    j=&i;

}//main

 i가 1000번지에 할당되었고, j가 1002번지에 할당되었다면 메모리의 구조는 다음
과 같다.

  그림 7-7 메모리의 구조

  즉 j에는 i의 주소 값 100050)이 들어 있다. j가 가리키는 값 - i값, 즉 1000

  50) 정수 1000과 주소 1000을 구분하기 위해, 문맥에 따라 [1000]을 주소 1000으로 사용한

    다.

번지에 있는 값 - 을 참조하기 위해서는 어떻게 할 것인가? 바로 역참조 연산자
*를 사용하는 것이다. *j는 j가 가리키는 값, 즉 정수 2를 의미한다. 아래 프로그
램의 결과를 주의해서 살펴 보라.

#include <stdio.h>

void main() {

    int i=2;

    int *j;

    j=&i;

    printf("%d,%p,%p\n",*j,j,&j);

}//main

  메모리의 구조가 위 그림과 같다면,

2,[1000],[1002]

가 출력될 것이다. 역참조 연산자는 오직 포인터 변수에만 쓸 수 있다. *i는 무의
미하며, 컴파일되지도 않는다.

  변수를 선언(declaration)하는 시점의 *와 사용(use)하는 시점의 *를 구분하라.

int *j;

는 j가 int를 가리키는 포인터 변수임을 의미한다. 하지만,

*j

라고 사용되면, j가 가리키는 값을 의미한다. j값을 사용하려면,

j

라고 사용한다.

  마지막으로 주의할 사항은, 문맥에 따라 & 은 비트 곱(bitwise AND) 연산자로
도 사용되며, * 은 곱하기(multiply) 연산자로도 사용된다는 것이다.

  관계(Relational)

==	같음 연산자(equality operator)

!=	안같음 연산자(inequality operator)

<	작다51) 연산자(less-than operator)

  51) 일반적으로 기준이 되는 수(number) n이 왼쪽에 있다고 본다. 그러므로 n < 의 형태이

    므로 작다(less than)라고 읽는다.

>	크다 연산자(greater-than operator)

<=	작거나 같다 연산자(less-than or equal operator)

>=	크거나 같다 연산자(greater-than or equal operator)

  수학에서는 관계 연산자의 결과가 참/거짓이지만, C에서는 1/0이다. 이것은 관
계 연산자가 수치 표현식(numerical expression)임을 의미한다. 비록 1을 참이라
고 간주해도 되지만, 정확한 것은 아니다. 1은 참이 아니라, 2바이트 정수 0000
0000 0000 0001(2)이다.

  ==은 할당 연산자 =와 구분하라.

i==j

와

i=j

는 매우 다르다. i==j는 i와 j의 값을 비교하여, 같으면 1, 아니면 0이 되는 표현
식이며, 이 표현식을 거친 후 i,j의 값에는 변함이 없다. 하지만, i=j는 j의 값을 i
에 할당하며, 표현식을 거친 후, i의 값은 j의 값으로 바뀐다. 만약

if (i==j)

라고 표현되어야 할 문장이,

if (i=j)

라고 표현되었다면 심각한 문제가 발생한다. 두 번째의 if문은 j가 0이 아니라면
조건은 항상 참(1)이다.

  !=는 수학의 ≠에서 유래(origin)하였다.

  >= 같은 연산자가 => 나 > = 처럼 표현되어서는 안 된다. 2개의 심벌이 1개
의 토큰을 이루는 경우이다. 아래 종합적인 예제를 참조하라.

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    printf("%d,%d,%d,%d\n",i>j, i==j, i!=j, j<=k);

    //       0  0  1  1

}//main

  sizeof

sizeof <expression>

sizeof ( <type> )

  sizeof 연산자는 표현식이나 형이 차지하는 바이트 수(number)를 구하기 위해
사용한다. sizeof 가 구하는 바이트 수가 형(type)에 관한 것이면, 반드시 괄호를
필요로 한다. 하지만, 표현식이라면 괄호가 없어도 된다. 그러므로, sizeof 2는
sizeof(2)52)와 동일하다. 하지만, sizeof int라고 쓸 수 없으며, sizeof(int)라고

  52) 이것이 함수 호출처럼 보이지만, 연산자임에 주의하라.

써야 한다.

  여러분이 사용하는 운영체제가 16비트 운영체제라면, 혹은 컴파일러가 16비트
용이라면, sizeof 2와 sizeof(int)의 값은 모두 2일 것이다53). 아래의 예제를 참

  53) int는 어떤 기계에서는 4바이트이다. 10진 상수 표현 2는 일반적으로 정수 상수이므로,

    int의 크기와 동일하다. 크기가 큰 정수형 상수를 명시적으로 선언하기 위해서는 접미어

    (postfix) L,UL등을 붙인다.

고하라. 이 예제는 16비트 컴파일러에서 컴파일 되었으므로, 결과는

2,2

2,4

이다.

#include <stdio.h>

void main() {

    int i,j;

    i=sizeof(int);

    j=sizeof i;

    printf("%d,%d\n",i,j);

    printf("%d,%d\n",sizeof 2,sizeof 2L);

}//main

  sizeof 는 동적 메모리 할당에서 크기를 얻기 위해, 빈번히 사용된다. 아래의
소스는 무엇이 잘못인가?

#include <stdio.h>

#include <alloc.h>

void main() {

    int *ip;

    ip=(int *)malloc(2);//왜 2바이트를 할당해야 하는가?

    *ip=2;

    printf("%d\n",*ip);

    free(ip);

}//main

  int가 2바이트라면, 위의 소스는 에러가 없다. 하지만, int가 4바이트인 컴파일
러에서 실행한다면, 실행 시(run time)에 에러가 발생할 것이다. 위의 소스는 아
래와 같이 고쳐서 사용하는 것이 마땅하다.

#include <stdio.h>

#include <alloc.h>

void main() {

    int *ip;

    ip=(int *)malloc(sizeof(int));

    *ip=2;

    printf("%d\n",*ip);

    free(ip);

}//main

  sizeof를 포인터 변수에 대해서 쓸 때 주의해야 할 사항이 있다. 아래의 소스는
어디가 잘못인가?

#include <stdio.h>

#include <alloc.h>

#include <string.h>

struct STest {

    int age;

    char name[80];

};//struct STest

void main() {

    struct STest *s;

    s=(struct STest*)malloc(sizeof(s));//s는 4바이트이다.

    s->age=30;

    strcpy(s->name,"seojt");

    printf("%s : %d\n",s->name,s->age);

    free(s);

}//main

  s는 포인터이므로 4바이트54)이다. 그러므로 sizeof(*s)가 되는 것이 맞다. 확

  54) 물론 메모리 모델(memory model)과 기계(machine)에 따라 2바이트가 될 수도 있다.

실하게 sizeof(struct STest)라고 하는 것이 좋다.

#include <stdio.h>

#include <alloc.h>

#include <string.h>

struct STest {

    int age;

    char name[80];

};//struct STest

void main() {

    struct STest *s;

    s=(struct STest*)malloc(sizeof(struct STest));

    s->age=30;

    strcpy(s->name,"seojt");

    printf("%s : %d\n",s->name,s->age);

    free(s);

}//main

  여러분이 sizeof 와 더불어 반드시 주의해야 할 사항은 size_t에 관한 것이다.
이 부분은 'new/delete 진보된 주제'에서 상세히 다룬다. 이 장의 이야기가 상당
히 길어 진 것 같다.

   형 변환(casting; type conversion)

//{{yey specified...

함수형 형변환(functional casting)

  정수형(int)의 변수 i를 문자형(char)의 변수 c로 변환하기 위한 문장은

    int i=65;

    char c;

    c=(char)i;

와 같다. 위의 형 변환 문장은 함수호출처럼 사용할 수 있다.

c=char(i);

  위의 문장이 비록 파라미터 i를 가지는 char()함수를 호출하는 것처럼 보이지
만, 위의 문장은 c=(char)i;와 같다. 즉, i의 형을 명시적으로 char로 변환하여 결
과를 c에 대입한다. 아래의 프로그램 조각은 A를 출력한다.

#include <iostream.h>

void main() {

    int i=65;

    char c;

    c=char(i);

    cout << c << endl;

}//main

결과는

A

이다.

   실습문제

1.


8-1. &가 있어야 하는가?

8-1. &가 있어야 하는
가?: 스택 동작(stack
operation)의 이해

  &1) 는 주소 연산자(address-of operator), C++의 참조 연산자

  1) 앰퍼샌드(ampersand)라고 읽는다.

(reference operator) 그리고 비트 곱(bitwise AND) 연산자로 사용된다.
이 장에서는 주소 연산자로서의 &만 다룰 것이다2). &는 모든 데이터 형

  2) &의 다른 역할은 '연산자', '참조(reference)와 별명'를 참고하라.

(type)의 앞에 붙여서 사용할 수 있으며, 이렇게 사용했을 때, &는 피 연산자
(operand)의 주소를 계산한다.

#include <stdio.h>

void main() {

    int i,*j;

    j=&i;//i의 주소가 j에 할당된다.

    *j=100;

    printf("%d\n",i);//100

}//main

  위 프로그램의 출력 결과는 100이다. i의 주소가 한 번 j에 지정되고 나면, i
와 전혀 상관없는 것 같이 보이는, j를 이용하여 i의 값을 바꿀 수 있다는 것
은 조금은 놀라운 일이다3). 위 프로그램의 메모리 구조는 아래와 같다.

  3) 이것은 C++에서 별명(alias)이란 이름으로 추가되었다. '별명'을 참고하라.

  그림 8-1 메모리 맵: i는 2바이트를 차지하지만, j는 4바이트를 차지한다.

  i는 정수이므로 2바이트를 차지하지만, j는 포인터이므로 4바이트를 차지하
고 있다는 것을 주목하라. 그렇다면, 아래의 문장이 무엇을 출력할지 예상해
보라.

printf("%p,%p,%p,%d\n",&i,j,&j,*j);

  포인터 값을 찍기 위해서는, %p를 사용한다. 이것의 출력 결과는

[1000],[1000],[1002],100

이 될 것이다. 이것의 실제 결과는 아래와 같다.

496F:2020,496F:2020,496F:201C,100

  16비트 IBM 운영체제에서는 세그멘테이션(segmentation)이라는 특별한
메모리 구조4)를 사용한다. 이것은 주소(address)를 나타내기 위해서, 세그먼

  4) 자세한 사항은 '메모리 모델'을 참조하라.

트의 시작 부분을 가리키는 16비트 레지스터인 베이스 레지스터(base
register)와 변위(offset)를 나타내는 변위 레지스터(offset register)의 쌍
(pair)으로 주소를 나타내는 방식이다. 그러므로 이러한 주소는 일반적으로
Base:Offset 형태로 적으며 이것은 일반적이다. 그러므로, 주소의 출력결과가
위에서처럼 출력되었다5). 독자들은 우리가 예상한 결과와 실제 결과 사이의

  5) 만약 여러분이 Win95 환경에서 32비트 모드로 컴파일 한다면, 16진수 ????????처럼

    출력될 것이다. 이것은 4바이트의 선형 주소(linear address)를 사용함을 의미한다. 또한

    16비트에서 메모리 모델이 small이라면, 16진수 4자리, 즉 16비트로 출력될 것이다. 이

    것은 16비트 컴파일러에서 near pointer로 알려져 있다.

중요한 차이점을 발견하였는가? 의문은 다음과 같다.

"그림에서 j의 주소는 i보다 2만큼 큰데, 왜 실제 출력 결과는 j의 주소가
4만큼 작은가?"

  위와 같은 의문은 독자들이 지역 변수가 할당되는 메모리 공간인 스택의 동
작을 정확히 이해하지 못하기 때문에 발생한다. 확실히 위의 <그림 8-2>는
잘못 그린 것이다. 하지만, 의미가 명확하다면, 이와 같은 그림을 계속해서 사
용할 것이다.

  블락 안에서 선언된 변수의 가시 범위(visibility scope)는 블락 안이다.
이것은 이 변수가 블락 안에서 사용되다가, 블락을 빠져 나왔을 때는 더 이상
사용되지 않는다는 의미이며, 이것은 블락 안의 문장이 실행될 때에만 변수가
메모리 할당된다는 의미이다. 이러한 변수는 자동6)으로 메모리에 할당되었

  6) 컴파일러가 이러한 일을 위해 코드를 생성한다. 프로그래머의 입장에서는 이러한 부

    분은 컴파일러의 자동화 부분에 속한다.

다가 블락을 탈출하는 순간 자동으로 메모리에서 해제되므로, 자동 변수
(auto variable)라고도 하며, 스택에 할당되므로 스택 변수(stack variable)
라고도 한다7).

  7) 변수의 종류에 대해서는 '변수의 종류'를 참조하라.

  블락 안에서 몇 개의 지역변수가 사용될지 알 수 없으므로, 스택을 이용한
메모리의 할당은 좋은 해결책이다. 어떤 블락이 3개의 지역 변수(local
variable)를 사용한다면, 컴파일러는 3개의 지역 변수를 스택에 할당한 다음,
스택의 상위에 자신이 할당한 변수의 개수 3을 푸시(push)함으로써 블락을 탈
출하는 순간, 스택의 상위에 push된 3을 보고, 블락에 진입하기 전의 메모리
상태를 회복할 수 있을 것이다.

  그림 8-2 스택에 n개의 지역 변수를 유지하는 법: 실제로는 지역 변수 외에도 여분의

    정보들이 스택에 저장된다. 어떤 함수 f()에서 m개의 지역 변수를 사용하고 있다. 이

    함수가 g()를 호출했을 때, g()는 n개의 지역 변수를 사용한다. g()가 빠져 나오기 전, n

    개의 지역 변수를 스택에서 해제함으로써 g()를 호출하기 전, f()의 상태로 복귀가 가능

    하다. g()가 n개의 변수를 해제하기 위해서는 단지 스택의 상위에서 값을 읽어서 이 값

    만큼만 팝(pop)하면 된다.

  스택이 메모리의 어느 부분이 될는지는 우리가 알 바 아니지만8), 일반적

  8) 이것은 컴파일러의 메모리 모델에 따라 다르다.

으로 메모리의 상위번지에 할당되어 상위 번지(high address)에서 하위(low)
번지로 자라난다. 이것은 먼저 선언된 변수가 먼저 할당이 되었을 때, 나중에
선언된 변수보다 상위 번지에 할당됨을 의미한다. 그러므로 위의 예에서 i가
1000번지에 할당되었다면9), j는 4바이트이므로 1000보다 4 작은 996번지에

  9) 물론 1000번지와 1001번지에 값이 할당된다.

할당될 것이다.

  독자들은 다음 문장을 기억하여 두라.

"블락 안에서 선언한 변수는 항상 스택에 자동으로 할당되며, 블락 범위를
벗어나는 순간 자동으로 해제된다."

  두 개의 변수의 값을 바꾸는(swap) 함수 Swap()을 만들어 보기로 하자. 이
함수는 두 개의 정수 a,b를 받아 a와 b의 값을 교환한다.

void Swap(int a,int b) {

    int t=a;//a의 값을 t에 보존한다.

    a=b;

    b=t;//보존된 a값을 b에 대입한다.

}//Swap

  위의 함수는 분명히 a와 b의 값을 바꾼다. 그렇다면, 아래 프로그램의 출력
결과를 쉽게 예측할 수 있을 것이다.

#include <stdio.h>

void Swap(int a,int b) {

    int t=a;

    a=b;

    b=t;

}//Swap

void main() {

    int a=2,b=3;

    Swap(a,b);

    printf("%d,%d\n",a,b);

}//main

  결과는

3,2

이다. 이 예측이 맞는지 직접 실행해 보라. 하지만, 놀랍게도 결과는 2,3이다.
왜 그런가? 바로 스택 동작 때문에 그렇다. 위 프로그램의 메모리 구조를 그
려보자.

  main() 함수의 블락에 진입했을 때, 블락 안에서 선언된 2개의 지역 변수는
스택에 할당될 것이다. 그리고 각각 2,3으로 초기화 될 것이다. 이 상태가 <그
림 8-3>에 나타나 있다.

  그림 8-3 main()에 진입했을 때 메모리 스택의 상태: a가 먼저 선언되었으므로, a가 먼

    저 스택에 push되었다.

  main()에서 Swap()을 호출하고 있다. Swap()이 3개의 지역 변수를 사용하
고 있다는 점에 주목하라. 비록 Swap()의 블락에서는 변수 t만 선언했지만,
파라미터로 넘겨온 변수 a, b역시 이 함수에서만 필요하므로 지역 변수로 스
택에 push된다. 또한 파라미터로 a가 먼저 명시되었지만, b부터 스택에 push
된다10). 그러므로 스택에는 차례대로 b,a,t가 push된다. 이 상태를 <그림

  10) 이것은 파라미터 전달방법(parameter passing method)이 C 방법을 사용하기 때

    문에 그렇다. 만약 _pascal을 명시한다면, a부터 push될 것이다. C방법은 _Cdecl을 사

    용하며, 이것이 기본값(default)이다. ANSI의 표준 키워드는 pascal과 cdecl을 사용한다.

8-4>에 나타내었다.

  그림 8-4 Swap()에 진입하는 순간 3개의 지역 변수가 차례대로 스택에 push, 즉 할당된

    다. main()에서 Swap(a,b)형식으로 호출했을 때, main()의 a의 값과 b의 값이 각각

    Swap()의 a,b에 복사(copy)되었다. 이러한 함수 호출 방식(function call method)을 값에

    의한 호출(call by value)이라 한다.

  먼저 a의 값이 t에 할당된다. a에는 b가 할당되므로 a=3이 된다. b에는 보
존해 둔 a값인 t가 할당되므로, b=2가 된다. 확실히 a와 b의 값이 바뀌지만,
이것은 Swap()의 a,b를 교환(swap)한다. main()의 a,b는 바뀌지 않는다. 이것
을 <그림 8-5>에 나타내었다.

  그림 8-5 Swap()의 a,b는 확실히 교환된다.

  이제 작업을 끝내고 Swap()을 빠져 나온다. 스택에 상단에 3이 있으므로,
이것을 pop한 후에 다시 3개를 pop할 것이다. 그러면 다시 아래와 같은 메모
리 상태가 될 것이다. 이것은 main()에서 Swap()을 호출하기 전의 메모리 상
태이다.

  그림 8-6 Swap()을 호출한 후의 메모리 상태, Swap()을 호출하기 전의 메모리 상태로

    복구되었다.

  그렇다면, main()의 지역 변수인 a와 b를 바꾸기 위해서는 함수로 무엇을
전달해야 하는가? 바로 그렇다. a의 주소와 b의 주소를 전달해야 한다. a의
주소와 b의 주소를 안다면, *a 혹은 *b를 이용해서 main()의 a,b를 수정할 수
있다. 프로그램은 아래와 같이 수정되어야 한다.

#include <stdio.h>

void Swap(int *a,int *b) {

    int t=*a;//a가 가리키는 값을 t에 대입한다.

    *a=*b;//b가 가리키는 값을 a가 가리키는 곳에 대입한다.

    *b=t;//t를 b가 가리키는 곳에 대입한다.

}//Swap

void main() {

    int a=2,b=3;

    Swap(&a,&b);//a와 b의 주소를 전달해야 한다.

    printf("%d,%d\n",a,b);

}//main

  main()에서 Swap()을 호출할 때, &a와 &b를 사용했다는데 주의하라. 이것
은 Swap()이 받는 파라미터 형(parameter type)이 int *이기 때문이다. 이제
Swap()은 a의 값과 b의 값을 전달받지 않는다. a와 b의 주소(address)를 전달
받는다. 주소를 저장하는 변수는 포인터(pointer)이다. 이 포인터는 정수(int)
를 가리키므로

Swap(int *a, int *b)

처럼 선언하는 것이 맞다. 위의 프로그램의 출력 결과는 이제

3,2

이다. 동작 과정의 메모리 상태를 살펴보자.

  main() 블락에 진입했을 때, 두 개의 지역 변수는 스택에 push된다. a가 스
택의 5000번지에 할당된다면, b는 다음에 push되므로, 4998번지에 할당될 것
이다. 그리고 각각의 변수는 2,3으로 초기화된다. 이 상태를 <그림 8-7>에 나
타내었다.

  그림 8-7 main()에서 2개의 변수가 스택에 push된다. 각 변수의 번지가 왼쪽 박스에 나

    타나 있다.

  main()에서 Swap()을 호출하고 있다. Swap()은 먼저 b를 push한다. b는 포
인터 변수이므로 4바이트가 할당될 것이다. 그러므로 이것의 주소는 4992번지
가 될 것이다. b가 정수가 아니라, 정수를 가리키는 포인터라는 것을 주의하
라. 즉 Swap()은 main()의 정수 b가 있는 곳의 주소인 &b=[4998]로 b를 초기
화한다. 다음에 a는 [5000]으로 초기화되며, t가 스택에 할당될 것이다.

  그림 8-8 Swap()의 a,b는 포인터 변수이므로, 각각 4바이트의 스택 메모리를 차지한다.

    t는 할당되었지만, 초기화되지는 않았다. 즉 쓰레기 값(garbage value)를 가지고 있다.

    스택의 상단에는 3이 push될 것이다.

  Swap()의 a는 main()의 a와 다르다. Swap()의 a는 main()의 a를 가리키고
있다. 아래 문장이 Swap()에서 실행된다면, 출력을 예상해 보라.

printf("%p,%p,%d",&a,a,*a);

출력은

[4988],[5000],2

이 될 것이다. <그림 8-9>를 참조하라.

  그림 8-9 Swap()의 a와 b는 main()의 a와 b를 가리키고 있다.

  main()의 a,b를 교환하기 위해서는 이제 간접 지정 연산자 *를 사용해야 한
다. 이 연산의 수행 결과 <그림 8-10>와 같이 될 것이다.

  Swap()의 a,b는 전혀 바뀌지 않았다는 것에 주목하라. Swap()의 어느 곳에
서도 a,b를 바꾸는 문장은 없다. Swap()은 *a 와 *b를 바꾸고 있으며, 이것은
a,b가 가리키는 값, 즉 main()의 a,b이다.

  그림 8-10 Swap()은 main()의 a,b를 교환한다.

  Swap()함수를 종료하는 순간 메모리의 상태는 Swap()을 호출하기 전의 메
모리 상태로 회복될 것이다. <그림 8-11>를 보라.

  그림 8-11 메모리가 Swap()을 호출하기 전의 상태로 돌아왔다. 하지만, a,b는 바뀌어져

    있다.

  함수를 호출할 때, 이렇게 변수의 값을 전달하는 것이 아니라, 변수의 주소
를 전달하는 것을 주소에 의한 호출(call by address)이라고 불러도 무난하
다. 이것이 참조에 의한 호출(call by reference)이 아니라는 것에 주의하라.
변수의 참조가 넘어간 것은 아니다, 단지 변수의 주소를 넘겨주어 참조의 효
과를 본 것이다. 후에 C++에서는 참조에 의한 전달11)이 문법적으로 추가되

  11) 이 부분은 '참조와 별명'을 참고하라.

었다.

  이제 정리하면, 독자들이 외워야 할 것은 다음 문장이다.

"함수의 파라미터로 넘어간 변수를 함수 안에서 바꾸었을 때, 함수를 빠져
나왔을 때에도 값이 바뀌어지도록 하려면, 변수의 주소를 전달해야 한다."

  scanf()에 &를 적어야 하는 이유

 이것은 여러분이 C를 배울 때 처음 접하게 되는 scanf()함수에서 변수의 주
소를 명시해야 하는 이유이다. 독자들은 키보드를 통해 정수를 한 개 입력받
는 함수를 아래와 같이 만들 수 있을 것이다12).

  12) 물론 이 문장은 컴파일 시간 경고(warning)를 발생한다.

    int i;

    scanf("%d",i);

  무엇이 잘못되었는가? 사용자가 입력한 변수는 scanf() 내부에서 분명히 i에
할당될 것이다. 즉 값이 변경될 것이다. 하지만, scanf()를 빠져 나오는 순간 -
위의 스택 동작을 그려 보라 - i는 스택에서 pop되므로, scanf()를 호출하기
전의 i값으로 남아 있을 것이다. scanf()에서 바뀐 값이 scanf()를 빠져 나왔을
때에도 바뀌게 하려면, i의 주소, &i를 전달해야 한다.

    int i;

    scanf("%d",&i);

하지만, 이미 i가 int *로 선언되었다면,

    int *i;

    ...

    scanf("%d",i):

처럼 사용하는 것이 맞다13). 마지막으로 한 가지 &의 사용에 대해 익혀두

  13) 물론 이 문장도 잘못된 곳이 있다. 포인터 변수가 가리키는 곳은 메모리 할당이 되

    어 있지 않으므로, 올바른 동작이 보장되지 않는다. 이 프로그램을 Win95에서 실행시

    키면, GPF(general protection fault)가 발생한다.

자. 아래에 설명하는 에러는 초보자들이 흔히 저지르는 실수 중의 하나이다.
이러한 에러를 범한 초보자들은 흔히 '컴파일러의 버그'로 컴파일러를 탓하지
만, 대부분은 독자들의 실수인 것이다.

  포인터를 바꾸려면?

  정수를 바꾸기 위해서 정수의 포인터(int *)를 전달했다. 하지만, 바꾸어야
하는 값이 정수의 포인터라면, 무엇을 전달해야 하는가? 그렇다. 정수의 포인
터를 가리키는 포인터(int **)를 전달해야 할 것이다. 먼저 아래 프로그램을
실행해 보라. 무엇이 잘못되었는가?

#include <stdio.h>

#include <alloc.h>

void main() {

    int *ip;

    ip=(int *)malloc(sizeof(int));

      //ip는 int *이므로 (int *)를 사용해서 명시적인 형변환이 반드시 필

      //요하다.

    *ip=100;

    printf("%d\n",*ip);

    free(ip);

}//main

잘못된 곳은 없으며, 결과는 100이 출력된다. ip는 포인터 변수이므로,
malloc() 혹은 C++의 new를 사용해서 메모리 할당 후에 사용하는 것이 맞다.
독자들이 처음으로 C를 접한다면, C++의 새로운 연산자 new에 익숙해지도록
하라14). new를 사용한다면, 독자들은 C++컴파일러를 가지고 있어야 하며,

  14) '연산자'를 참조하라.

소스를 저장할 때, 확장자가 .CPP가 되도록 저장해야 한다. new를 사용해서
수정한 소스는 아래와 같다.

#include <stdio.h>

void main() {

    int *ip;

    ip=new int;//int만큼, 즉 2바이트를 할당해서 시작 주소를 ip에 대입한

               //다.

    *ip=100;

    printf("%d\n",*ip);

    delete ip;

}//main

  이제 메모리 할당하는 부분을 하나의 함수로 만들기로 했다. 그래서 소스를
아래와 같이 수정하였다. 확실히 아래의 소스는 틀린 곳이 있다. 어디인가?

#include <stdio.h>

void f(int *ip) {

    ip=new int;

}//f

void main() {

    int *ip;

    f(ip);

    *ip=100;

    printf("%d\n",*ip);

    delete ip;

}//main

  f()에서 바꾸어야 하는 값은 ip가 가리키는 값(int)이 아니라, ip(int *)이다.
정수를 바꾸어야 한다면, 정수 포인터를 전달하는 위 함수 f()는 바르게 동작
한다. 하지만, 지금은 정수 포인터(int *)를 바꾸어야 하므로, 함수 f()는 틀렸
다. 그러므로 f()는 정수 포인터의 포인터를 전달받아야 하며, main()에서 f()
를 호출할 때 파라미터에는 ip를 전달해서는 안되고, ip의 주소인 &ip를 전달
해야 한다. &ip는 정수 포인터의 포인터이므로 f()쪽에서

int **

처럼 선언해야 한다. 수정된 소스는 아래에 리스트하였다.

#include <stdio.h>

void f(int **ip) {

    *ip=new int;

}//f

void main() {

    int *ip;

    f(&ip);//ip의 주소를 넘겨 주어야 한다.

    *ip=100;

    printf("%d\n",*ip);

    delete ip;

}//main

  위 프로그램의 동작과정을 메모리로 그리지는 않는다. 독자들이 스택을 그
리면서 손으로 직접 실행해 보기 바란다.

  실습문제

1. 다음 프로그램의 잘못된 부분을 수정하라.(Hint: 포인터 변수는 사용하기
전에 할당(allocation)해야 한다)

#include <stdio.h>

#include <alloc.h>

void main() {

    int *i;

    scanf("%d",i);

}

2. 아래 프로그램의 잘못된 부분을 수정하라.

#include <stdio.h>

void f(int **ip) {

    *ip=new int;

}//f

void main() {

    int **ip;

    f(&ip);

    **ip=100;

    printf("%d\n",**ip);

    delete ip;

}//main


8. 제어구조

8. 제어구조(control
structure)

  독자들은 GW-BASIC이란 고전 언어를 써 본 사람이 있는가? 옛날의 베이
직 언어에서 흐름(flow)을 제어하는 유일한 방법은 GOTO였다. <그림 8-1>
에서 GOTO문이 얼마나 프로그램을 복잡하게 만드는지 살펴 보라.

  그림 8-1 GOTO의 난잡한 사용

  이러한 GOTO의 난잡한 사용은 1960년대 초반, 하드웨어의 급속한 성장을
소프트웨어가 따라가지 못하는 소프트웨어의 위기(software crisis)를 맞게
되었다. 물론 그 이후에 그 상황에 붙여진 이름이었지만, 이를 극복하기 위한
많은 노력이 학자들에 의해 이루어졌다. 우리가 당연하게 사용하고 있는 if,
while 같은 문장들이 그 당시에는 전혀 유용하지 않았다. 없었던 것은 아니었
지만, 에러의 국부성(locality)이 보장되지 않았다. while문 안에서 발생한 에
러는 while에만 영향을 끼쳐야 한다. 어떻게 GOTO로 할 수 있는 모든 일을
하면서, 에러의 국부성을 보장할 것인가? 바로 블락(block)이라는 개념과 제
어 구조(control structure)가 문제를 해결하는 열쇠였다.

  최근의 언어들 - C, C++ - 은 블락과 제어 구조를 지원하는데, 이처럼 블
락과 제어구조를 지원하는 언어를 구조적 언어(structured programming
language)라 한다. C++는 구조적 언어에 클래스와 상관된 개념이 결합된 객
체 지향 언어(object oriented language)이다. 물론 객체 지향 언어는 구조
적 언어이다. 하지만, 구조적 언어가 객체 지향 언어인 것은 아니다. 이 세상
에 존재하는 모든 구조적 언어는 흐름을 제어하기 위해 3가지의 흐름 제어
구조(flow control structure)만을 지원한다. 그것이 ①순차 ②비교 ③반복
구조이다1).

  1) 이것은 여러분이 어떠한 언어를 처음 접하게 되었을 때, 살펴보아야 할 제어구조가

    3가지뿐임을 의미한다.

  그림 8-2 제어 구조

  순차(sequential) 구조는 직관적(intuitive)이다. 그것은 실행단위 - 예를 들
면 문장(statement) - 가 위에서 아래로 실행됨을 의미한다. 즉 아래서 위로
실행되지 않는다는 의미이다. 이것은 당연한 것 같지만, 문법에 의해서 정의되
기 때문에 그런 것이다. 실행 단위가 반드시 문장이 아님에 유의하라. 한 실행
단위는 다른 순차 구조를 호출할 수도 있다.

  그림 8-3 순차 구조의 의미

  실제로 실행 단위는 블락(block)이다. 블락 안에서 발생한 에러는 전역변수
(global variable)를 건드리지 않는다면, 외부에 영향을 미치지 않는다. 이것은
복귀 에러(regression fault)2)가 발생함을 미연에 방지한다.

  2) 에러가 발생한 소스의 일부를 고쳤을 때, 이와 전혀 상관이 없는 부분에 에러가 연

    달아 발생한다면, 이를 복귀 에러라 한다. 구조적 언어에서도 복귀 에러가 발생하지 않

    도록 조심스럽게 코딩하는 것이 필요하다. 이러한 복귀 에러는 클래스의 추상화를 사

    용한다면, 상당히 줄일 수 있다.

  비교(conditional) 구조는 특정한 조건을 비교하여 조건의 결과인 참/거짓에
따라 실행할 문장을 결정한다. 비교를 지원하는 제어문에는 if, switch가 있다.

  반복(iterational) 구조는 특정한 조건이 참/거짓인 동안 문장을 실행한다. 반
복을 지원하는 제어문에는 for,while과 do가 있다.

  제어 구조를 지원하는 문장이 5가지뿐이라는 사실은 놀랍지 않은가? 실제
로 이 5개의 문장으로 GOTO의 모든 역할을 대신할 수 있으며, 이것은 수학
적으로 증명 가능하다. C가 goto를 지원하지 않는 것은 아니다. 하지만, 절대
로 사용하지 말라3).

  3) goto를 사용해도 괜찮을 만한 상황이 몇 있기는 하다. goto문이 전방이나 후방으로의

    유일한 goto문이라면, 흐름을 파괴하지 않는다. 여러 개의 겹쳐진 블락을 탈출하기 위

    해서는 복잡한 여러 개의 플래그(flag) 변수를 사용하는 것보다, goto가 효과적이다.

  5가지 제어문들의 문법은 각각 다음과 같다.

(1) if문

    if (표현식1)

        문장1;

    [else if (표현식2)

        문장2;][...]

    [else

        문장3;]

(2) switch문

    switch (표현식) {

        [case 표현식:

            문장;

            [break;]][...]

        [default:

            문장;

            [break;]]

    }

(3) for문

    for ([표현식1]; [표현식2]; [표현식3])

        문장;

(4) while문

    while (표현식)

        문장;

(5) do문

    do

        문장;

    while (표현식);

  if

  if문은 문법이 상당히 복잡한 것처럼 보이지만 실제로 그런 것은 아니다.

    if (표현식1)

        문장1;

    [else if (표현식2)

        문장2;][...]

    [else

        문장3;]

  []는 그 부분이 생략될 수 있음(option)을 의미한다. [...]는 앞부분이 0번 이
상의 반복 가능함(0 or more repetition)을 의미한다. 이것은 else if 부분이 없
어도 되고, 또한 여러분 반복되어도 가능함을 의미한다. 마지막의 else부분은
선택 사항이므로 없어도 된다. 하지만 있다면 반드시 1번만 있어야 한다.

             위의 그림은 언어의 문법을 나타내는 도식(diagram)이다. 실제로 C++ 언어
          의 문법은 CFG(context free grammar)를 이용하여 나타낼 수 있다. 하지만,
          이 그림은 CFG와 대등하며, 초보자들이 CFG보다 이해하기 쉽다. CFG에
          관해서는 'CFG'에서 자세히 다루며, 언어의 문법은 필요한 경우만 설명하
          지만 이러한 도식은 사용하지 않는다. 하지만 독자들은 이러한 도식이 어
          떤 문법을 나타내는지 알아야 하며, C++의 문법을 이러한 도식으로 표현
          할 수 있어야 한다. 실제로 위의 문법은 '오토마타'에서 문법으로 다루고
          있다.

  if나 else if에서 조건을 판단하는 부분은 ()안의 표현식이다. 괄호 안이 표
현식임에 유의하라. 표현식이 0이면 if는 거짓으로 간주한다. 0이 아니면, 참으
로 간주한다. 표현식1이 참(not 0)이라면 문장1을 수행한다. 그리고 if문을 종
료한다. 표현식1이 거짓이라면 else if의 표현식2를 검사한다. 이 문장이 참이
면 문장2를 수행한다. else if의 모든 조건이 다 거짓이라면, 마지막 else에 지
정된 문장3을 수행한다. 독자들이 주의해야 할 사항은 2가지이다.

  (1) if나 else if에서 명시된 표현식이 2가지 이상이 참이라면 제일 먼저 만난
표현식이 평가되므로, 2번째 이하부터의 문장은 실행되지 않는다. 즉 여러 개
의 조건이 참일지라도 첫 번째 조건만이 실행된다.

  (2) else가 없는 if문일 경우, 한 문장도 실행되지 않을 수도 있다.

  (3) else가 있는 if문은 최소한 1문장은 실행된다.

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    if (i<j)

        printf("i is less than j\n");//이 문장이 실행된다.

}//main

  위의 예에서, 단독으로 사용된 if는 조건이 만족되므로, printf()가 실행된다.

#include <stdio.h>

void main() {

    int i=5,j=3,k=4;

    if (i<j)

        printf("i is less than j\n");

    else

        printf("i is greater or equal than j\n");//이 문장이 실행된다.

}//main

  위의 예에서 if의 조건은 거짓(0)이므로, else 의 printf()가 실행된다. else가
있으면,

#include <stdio.h>

void main() {

    int i=5,j=4,k=3;

    if (i<j)

        printf("i is less than j\n");

    else if (j<=k)

        printf("j is less than or equal to k\n");

    else if (i==k)

        printf("i is equal k\n");

}//main

  위의 예에서는 if나 else if의 어느 조건도 거짓이므로, 한 문장도 실행되지
않는다. 하지만, 아래의 예에서는 두 조건이 참이지만, 첫 번째 비교된 문장만
이 실행된다는데 주의하라.

#include <stdio.h>

void main() {

    int i=2,j=3,k=2;

    if (i<j)//이 조건은 참이다.

        printf("i is less than j\n");//이 문장이 실행된다.

    else if (j<=k)

        printf("j is less than or equal to k\n");

    else if (i==k)//이 조건도 참이다.

        printf("i is equal k\n");

}//main

  즉, 마지막 else if (i==k)가 참임에도 불구하고, 첫 번째 if (i<j)의 조건이
만족되므로, 마지막의 else if (i==k)는 평가조차 되지 않는다. if문은 만족되는
조건이 여러 개 일지라도, 한 문장밖에 실행되지 않는다.

  아래의 예에서는 else가 쓰인다면, 최소한 1문장은 실행된다는 것을 보여준
다.

#include <stdio.h>

void main() {

    int i=5,j=4,k=3;

    if (i<j)

        printf("i is less than j\n");

    else if (j<=k)

        printf("j is less than or equal to k\n");

    else if (i==k)

        printf("i is equal to k\n");

    else

        printf("i is greatest\n");//이 문장이 실행된다.

}//main

  if 문의 조건이 만족되면 실행되는 문장은 1문장이라는 것에 주의하라. if의
조건이 만족되었을 때, 2문장 이상이 실행되게 하려면, 블락(block: 복합
문;compound statements)을 만들어야 한다. 블락은 여러 개의 문장을 하나
의 문장처럼 만들어 준다.

#include <stdio.h>

void main() {

    int i=5,j=4;

    if (i<j)

        printf("i is less than j\n");

        printf("this string will be printed always\n");

}//main

  위의 예에서 두 번째 printf()는 항상 실행된다. 단순한 들여쓰기
(indentation)가 문법을 결정짓는 것은 아니다. 사용자의 의도대로라면, 두 문
장을 하나의 문장처럼 만들어 주어야 한다. 블락을 이용하여 고쳐진 소스는
아래와 같다.

#include <stdio.h>

void main() {

    int i=5,j=4;

    if (i<j) {

        printf("i is less than j\n");

        printf("this string will be printed always\n");

    }//if

}//main

  이러한 블락이 if에만 적용되는 사항이 아니라, 일반적이라는 사실을 잊지
말고 기억하기 바란다.

  독자들이 if문을 다루면서, 마지막으로 알아야 하는 주제는 달랑거리는
else-문제(dangling else-problem)이다. 아래의 예에서 else는 어느 if와 짝
지어져도 문법적인 에러가 아니다. 그러면 else를 누구와 - 어느 if와 - 짝 지
워야 하는가? 바로 이 문제가 달랑거리는 else-문제이다.

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    if (i<j)

        printf("i is less than j\n");

        if (j<k)

            printf("j is less than k\n");

    else//어느 if와 짝인가? if (i<j)인가? if (j<k)인가?

        printf("which if is my partner?\n");

}//main

  이 문제의 해결 방법은 알려져 있으면, 이 세상에 존재하는 모든 컴파일러
는 이 해결 방법을 사용한다. 이 해결 방법의 이름은 '결혼하지 않은 가장 가
까운 이웃(not married nearest neighbor)'이며, 해결 방법은 다음과 같다.

"else의 짝은, else를 만나기 전 거친 if중, else와 아직 짝이 없으면서,
가장 가까운 if이다."

  위의 예에서 가장 가까운 짝이 없는 if는 if (j<k)이므로 이 문장과 짝 지워
진다. 그러므로, 위 프로그램의 출력 결과는

i is less than j

j is less than k

이다. 이러한 규칙이 독자들을 헷갈리게 한다면, 명시적인 블락으로 이를 구분
하는 것이 좋은 방법이다.

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    if (i<j) {

        printf("i is less than j\n");

        if (j<k)

            printf("j is less than k\n");

        else

            printf("which if is my partner?\n");

    }//if

}//main

  사용자의 의도가 else를 첫 번째 if와 짝 짓는 것이었다면, 아래와 같이 블
락을 만든다.

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    if (i<j) {

        printf("i is less than j\n");

        if (j<k)

            printf("j is less than k\n");

    } else

        printf("which if is my partner?\n");

}//main

  출력 결과는 우연히 위와 같아졌지만, 의도가 같았던 것은 아니다.

  switch

    switch (표현식) {

        [case 표현식:

            문장;

            [break;]][...]

        [default:

            문장;

            [break;]]

    }

  if문으로 비교 문장을 작성할 경우, 같은 변수에 대한 고정 값(fixed value)
- 범위가 아닌 값 - 으로 비교를 행해야 할 경우가 종종 발생한다. 이 때는
switch 문을 사용하는 것이 훨씬 읽기(readability) 쉽다. 아래의 예에서, 사용
자가 입력한 문자(character)가 a, b 혹은 c인지 검사한다.

#include <stdio.h>

#include <conio.h>

void main() {

    int i;

    printf("Enter a char:");

    i=getch();

    if (i=='a')

        printf("a pressed\n");

    else if (i=='b')

        printf("b pressed\n");

    else if (i=='c')

        printf("c pressed\n");

    else

        printf("whick key pressed?\n");

}//main

  위 문장이 틀린 것은 아니지만, switch를 사용하여 아래와 같이 대등하게
바꿀 수 있다.

#include <stdio.h>

#include <conio.h>

void main() {

    int i;

    printf("Enter a char:");

    i=getch();

    switch (i) {

        case 'a':

            printf("a pressed\n");

            break;

        case 'b':

            printf("b pressed\n");

            break;

        case 'c':

            printf("c pressed\n");

            break;

        default:

            printf("whick key pressed?\n");

            break;

    }//switch

}//main

  switch 는 괄호 안의 표현식을 평가하여, 평가 값이 case 뒤에 명시된 값과
일치하는지 검사한다. 값이 일치하면, case 이후의 문장을 실행하기 시작한다.
break를 만나면 switch문을 탈출(exit)한다.

  여러분은 콜론(colon : )으로 끝나는 문장이 라벨(label) 취급됨을 주의해야
한다. 라벨은 실행 문장이 아니다. 단지 컴파일 시간에 goto4)가 분기해야

  4) goto문은 설명하지 않지만, goto를 사용하기 위해서는 반드시 라벨을 사용해야 한다.

할 주소를 결정짓기 위해 사용된다. 실제로 switch 문장은 컴파일러에 의해
goto문장으로 번역된다. 그리고 switch문장에서 사용된 break를 만나면, 무조
건 switch의 끝으로 분기(jump)한다. 즉 switch문을 탈출한다.

  switch의 괄호 안에 쓰이는 것이 표현식임에 주의하라. 즉 위의 소스는 다
음과 같이 수정할 수 있다.

#include <stdio.h>

#include <conio.h>

void main() {

    int i;

    printf("Enter a char:");

    switch (i=getch()) {

        case 'a':

            printf("a pressed\n");

            break;

        case 'b':

            printf("b pressed\n");

            break;

        case 'c':

            printf("c pressed\n");

            break;

        default:

            printf("whick key pressed?\n");

            break;

    }//switch

}//main

  또한 break는 선택 사항이므로 프로그래머의 의도에 따라 생략할 수 있음
을 주목하라. 위의 소스는 대문자인 경우는 default:가 실행된다. 대문자인 경
우도 의도대로 동작하기 위해서는 소스를 아래와 같이 수정한다. 또한 마지막
의 break는 생략해도 된다는 것을 알 것이다.

#include <stdio.h>

#include <conio.h>

void main() {

    int i;

    printf("Enter a char:");

    switch (i=getch()) {

        case 'A':

        case 'a':

            printf("a pressed\n");

            break;

        case 'B':

        case 'b':

            printf("b pressed\n");

            break;

        case 'C':

        case 'c':

            printf("c pressed\n");

            break;

        default:

            printf("whick key pressed?\n");

            //break;//마지막의 break는 생략 가능하다.

    }//switch

}//main

  이런 질문을 받은 적이 있다.

  'default가 꼭 마지막에 와야 합니까?'

  그런 것은 아니다. 하지만, 마지막에 사용할 것을 권장한다.

switch를 보는 컴파일러의 입장

  마지막으로 독자들이 switch를 사용하여 문장을 작성할 때, 사소한 주의 사
항이 있다. switch 문을 컴파일러가 어떻게 번역하는가? 이것이 단순히 if...
else if...로 번역할 것이라는 생각은 큰 오산이다. 이것을 번역하는 방법은 3가
지가 있다.

    ■ 단순히 if... else if... 구조로 번역한다.

    ■ Jump table을 사용한다.

    ■ Hashing table을 사용한다.

  위에 관한 자세한 사항은 언급하지 않는다. 하지만, Jump table로 구현되었
을 때, 최적의 속도를 낸다. 이렇게 하기 위해서는 case 가 비교하는 상수는
연속된 값이어야 한다. 조언은 다음과 같다.

"switch의 case에는 되도록 연속된 값을 사용하라."

  for

    for ([표현식1]; [표현식2]; [표현식3])

        문장;

  for문은 정해진 반복을 수행하기 위해 사용하지만, 반드시 그런 것은 아니
다. 특정 문장을 10번 수행해야 한다면, for문은 최상의 선택이다. 하지만, 특
정한 조건이 참인 동안 반복을 수행하기 위해서 for문을 사용할 수 없는 것은
아니다.

  for문은 다음과 같이 동작한다.

  ① for문에 들어오기 전 표현식1을 실행한다.

  ② 표현식2를 평가한다. 참이면, 단계 ③으로 간다. 아니면, for를 탈출한다.

  ③ 문장을 수행한다.

  ④ 표현식3을 수행한다.

  ⑤ 단계 ②로 간다.

  이와 같은 for문의 특징을 적용하여, for의 문법을 새로 쓰면, 아래와 같다.

    for ([초기문]; [비교문]; [증가문]5))

  5) 일반적으로 비교문에서 사용할 변수를 증가시키는데 사용하므로, 증가문이라고 쓴다.

        문장;

  for문의 표현식들은 생략할 수 있다는 것에 주목하라6). 그러므로 위의 문

  6) 문장은 생략해도 되지만, ; 은 생략할 수 없다.

장은

    초기문;

    for (;[비교문];) {

        문장;

        증가문;

    }

과 동일하다7). 아래의 예제는 1부터 10까지의 정수를 출력한다.

  7) 물론 블락으로 두 문장을 감싸야 한다.

#include <stdio.h>

void main() {

    int i;

    for (i=1;i<=10;++i)

        printf("%d\n",i);

}//main

  아래의 예제는 위와 동일하게 출력한다.

#include <stdio.h>

void main() {

    int i;

    i=1;

    for (;i<=10;) {

        printf("%d\n",i);

        ++i;

    }//for

}//main

  비교문 자체도 생략할 수 있음에 주목하라. 비교문이 생략되면, 컴파일러는
참으로 간주한다. 그러므로 아래의 코드는 무한 루프(infinite loop)를 만든
다.

    for (;;)

        문장;

  1부터 100까지의 합을 구하는 프로그램8)을 for를 사용하여 만들어 보자.

  8) 실제로 프로그램을 이렇게 작성하지는 않을 것이다. 1부터 n까지의 합을 구하는 공

    식 n(n+1)/2를 사용하면, O(1)만에 구할 수 있다.

#include <stdio.h>

void main() {

    int s=0,i;

    for (i=1;i<=100;++i)

        s=s+i;

    printf("%d\n",s);

    //       5050

}//main

  위의 예에서 for문을 탈출했을 때의 i는 101이라는 사실을 주의하라. 경계
조건(boundary condition)은 모든 제어문에서 항상 중요하다.

  또한, 반복문 - for,while과 do문 - 에서 중요한 사항은 break와 continue
에 관한 것이다. 각각의 용도는 아래와 같다.

  ① break는 자신을 둘러싼 가장 가까운 반복문을 탈출한다.

  ② continue는 자신을 둘러싼 가장 가까운 반복문의 처음 - 조건 검사 부분
- 으로 분기한다.

  그림 8-4 break와 continue의 역할

  for문에서는 continue가 바로 비교문으로 분기하지 않는다. 마지막 증가문을
실행한 다음, 비교문으로 분기한다. 아래의 소스는 1부터 10까지를 출력하지
만, 5는 출력하지 않는다.

#include <stdio.h>

void main() {

    int i;

    for (i=1;i<=10;++i) {

        if (i==5) continue;

        printf("%d\n",i);

    }//for

}//main

  위의 소스는 continue의 특징 때문에 아래와는 다르다는 것을 주의하라.

#include <stdio.h>

void main() {

    int i;

    i=1;

    for (;i<=10;) {

        if (i==5) continue;

        printf("%d\n",i);

        ++i;

    }//for

}//main

  흔히 범하는 오류는 break나 continue가 블락을 탈출한다고 생각하는 것이
다. 아래의 문장에서 break는 if를 탈출하는가?

#include <stdio.h>

void main() {

    int i;

    for (i=1;i<=10;++i) {

        if (i==5) {

            i=i+1;

            break;

        }//if

        printf("%d\n",i);

    }//for

}//main

  그렇지 않다. break는 자신을 둘러싼 가장 가까운 반복문을 탈출한다. 그러
므로, 위 프로그램의 출력결과는

1

2

3

4

이다. 만약 break를 둘러 싼 가장 가까운 반복문이 없다면, 그것은 에러이다.
아래 문장은 컴파일 시간 에러가 발생한다.

#include <stdio.h>

void main() {

    int i=2,j=3;

    if (i<j) {

        if (i==1)

            break;//break를 둘러 싼 반복문이 없다.

        printf("i is greater than 1 and less than j\n");

    }//if

}//main

/* Error: Misplaced break in function main() */

  그렇다면 연속된 두 개의 블락을 탈출하기 위해서는 어떻게 할 것인가? 문
법적인 방법은 없다9). 유일한 해결책은 깃발 변수(flag variable)10)를

  9) goto를 사용하면 가능하다.

  10) 변수의 목적이 상태의 ON/OFF를 구분하기 위해 사용된 변수를 깃발 변수라 한다.

    이 변수의 이름은 기차역에서 역무원이 깃발을 들어 철로의 상태를 알려 주는 것에서

    비롯한다.

사용하는 것이다.

#include <stdio.h>

#include <conio.h>

void main() {

    int i,j,flag=0;

    for (i=2;i<=100;++i) {

        for (j=2;j<=9;++j) {

            if (kbhit()) {

                flag=1;

                break;

            }//if

            printf("%d*%d=%d\n",i,j,i*j);

        }//for

        if (flag==1)

            break;

    }//for

}//main

  위 프로그램은 2부터 100단까지의 구구단을 출력하다가, 사용자가 임의의
키를 누르면, 프로그램을 종료한다. flag=0으로 초기화된다. 이것은 탈출 조건
이 성립하지 않음을 나타낸다. 임의의 키가 눌려지면, 안의 for문에서 flag=1
로 결정(set)된다. 그리고 안의 for문을 탈출한다. 바깥의 for문에서 flag는 검
사되고, flag=1이므로 if의 조건이 만족되어 바깥 if문도 탈출한다. flag는 오로
지 if문을 탈출하기 위한 상태를 기록하기 위해 사용되었다. 이러한 변수를 깃
발 변수라 한다.

  while

    while (표현식11))

  11) 표현식은 비교문이다. 이 문장이 참이면, while의 문장은 실행된다.

        문장;

  while문은 표현식이 참(1)인 동안 문장을 실행한다. for문과는 다르게 표현
식을 생략할 수 없다. 무한 루프를 만들기 위해,

    while ()

        문장;

이라고 해서는 안 된다. 이것은 문법 에러(syntax error)이다. 무한 루프는 표
현식에 항상 참인 문장 - 일반적으로 1 - 을 적어줌으로써 가능하다.

    while (1)

        문장;

위의 문장은 무한 루프를 만드는 일반적인 방법이다. 아래의 소스는 1부터 10
까지의 정수를 출력한다.

#include <stdio.h>

void main() {

    int i=1;

    while (i<=10) {

        printf("%d\n",i);

        ++i;

    }//while

}//main

while문을 탈출했을 때, i의 값이 11이라는 것에 주의하라. 종료 조건은 항상
중요하다. 이러한 탈출후의 변수 값은 여러 알고리즘에 중요하게 사용한다.

  while문 역시 반복문이므로 break와 continue를 가질 수 있다. break는
while문을 탈출한다. continue는 while문의 처음 - 표현식이 있는 조건 검사문
- 으로 분기한다. continue를 만나자마자 처음으로 분기하므로, 조심스럽게 코
딩하지 않으면, 귀찮은 일들이 발생한다. 아래의 예는 이것을 보여준다.

#include <stdio.h>

void main() {

    int i=1;

    while (i<=10) {

        printf("%d\n",i);

        if (i==5) continue;//i가 증가하지 않으므로 무한루프에 빠진다.

        ++i;

    }//while

}//main

  위의 예에서 if의 continue는 while의 처음 비교문으로 분기하므로, 이후의
문장인

++i;

는 실행되지 않는다. 결과는 1,2,3,4까지 찍힌 후 무한 루프에 빠진다. 프로그
래머의 의도가 5만 찍지 않을 의도였다면, 소스를 아래와 같이 수정할 수 있
다.

#include <stdio.h>

void main() {

    int i=0;

    while (++i<=10) {

        if (i==5) continue;

        printf("%d\n",i);

    }//while

}//main

  1부터 인쇄하기 위해 i=0으로 설정되었다는데 주의하라. 원하는 조건이 발
생했을 경우, 문장을 실행하지 않기 위해서, if와 continue는 자주 결합되어 빈
번히 사용된다.

  do

    do

        문장;

    while (표현식);

  while과 do문은 매우 비슷하다. 하지만, while이 조건을 먼저 검사하고 문장
을 실행하는 반면, do문은 문장을 먼저 실행하고 표현식을 검사한다. 이것에
의해 미소한 차이가 발생한다. 아래의 예는 1부터 10까지의 정수를 출력한다.

#include <stdio.h>

void main() {

    int i=1;

    do {

        printf("%d\n",i);

        ++i;

    } while(i<=10);

}//main

  continue가 do쪽으로 분기하는 것이 아니라, while의 조건문 쪽으로 분기한
다는데 주의하라. 5를 빼고 10까지 출력하기 위해서는 소스를 아래와 같이 작
성하여야 한다.

#include <stdio.h>

void main() {

    int i=0;

    do {

        ++i;

        if (i==5) continue;

        printf("%d\n",i);

    } while(i<10);

}//main

  i를 먼저 증가시키기 때문에 i=0으로 초기화하였고, 증가된 i를 인쇄하기 때
문에, 조건 검사가 i<10으로 바뀌었다. while문을 탈출했을 때, i=10이다.

  while문은 조건 검사가 먼저 이루어지므로, 문장이 1번도 실행되지 않을 수
도 있다. 하지만, do문은 최소한 1번은 반드시 실행된다. 아래의 예에서, while
의 문장들은 실행되지 않는다. 하지만, do문은 1번 실행된다.

#include <stdio.h>

void main() {

    int i;

    i=10;

    do {

        printf("%d\n",i);

        ++i;

    } while(i<10);

    i=10;

    while (i<10) {

        printf("%d\n",i);

        ++i;

    }//while

}//main

  프로그래머의 의도에 따라 while과 do는 적절히 이용된다.

  이로써 모든 제어문의 설명을 마쳤다. 이것들을 이해하는 것은 중요한 것이
아니다. 이 문장의 구조를 이용하여, 문제를 풀 수 있는 능력을 기르기 위해서
는 이 책의 나머지 부분을 3달 안에 다 읽고 이해하겠다는 노력이 필요하다.

  연습문제

1.


9. 프로젝트 만들기

9. 프로젝트(project) 만
들기

  프로그램 한 개를 만들기 위해 여러 개의 소스가 필요한 경우, 프로젝트 파
일을 만들어야 한다. 아마도 여러 개의 소스인 경우는 큰 프로젝트 프로그램
일 것이다. 그래서 이것을 프로젝트 파일이라고 한다1). 프로젝트에는 소스 프
  1) 윈도우 응용 프로그램인 경우 반드시 여러 개의 파일 - *.RC, *.DEF 등 - 이 필요하

    므로, 반드시 프로젝트를 만들어야 한다.

로그램들의 관련 정보가 포함되어 컴파일 시에 이러한 정보들을 참고한다.

  우리가 예로 만들어 볼 프로젝트는 다음 3개의 파일과 관련이 있다. 첫 번
째 파일은 sub.cpp인데 전역 변수와 함수가 각각 1개씩 있다.

//sub.cpp

int global=100;

void swap(int *i,int *j) {

    int t=*i;

    *i=*j;

    *j=t;

}//swap

  함수와 변수는 쓰기 전에 선언되어야 한다. 위 파일을 사용하는 프로그램마
다 전역변수 global과 함수 swap()에 대해 선언을 포함해야 한다. 이것은 번
거로운 일이다. 그래서 이러한 선언을 모아둔 파일을 별도로 만들어 이것을
포함(include)하도록 구현한다. 두 번째 파일은 sub.h이다2).

  2) 혼돈을 피하기 위해 sub.cpp의 선언을 모아둔 파일은 sub.h로 하는 것이 좋다.

//sub.h

extern3) int global;

  3) 다른 파일에 있는 전역 변수이므로, extern을 붙여야 한다. 전역 변수에 static 키워드

    가 명시되지 않으면, 전역 범위(global scope)를 가진다. static 키워드가 명시되지 않은

    전역변수를 다른 파일에서 참조하기 위해서는 extern을 사용한다. 하지만, 만약 전역

    변수가 static으로 선언되었다면, extern을 사용해도 그 변수를 참조할 수 없다. 이러한

    전역 변수는 파일 범위(file scope)를 가진다.

void swap(int *i,int *j);

마지막으로 sub.cpp를 이용하는 main.cpp이다.

//main.cpp

#include <stdio.h>

#include "sub.h"

void main() {

    int i=2,j=3;

    swap(&i,&j);

    printf("%d,%d,%d\n",i,j,global);

    //       3 2 100

}//main

  프로젝트 파일에는 sub.cpp와 main.cpp가 지정되어야 한다. sub.h는 지정될
필요가 없다는데 주의하라. sub.h는 main.cpp의 2번째 줄에서 포함하고 있다.

  문제는 여기에 있다. 어떻게 main()에서 사용하는 swap()이라는 함수가
sub.cpp에 있다는 것을 알려 주는가? 바로 프로젝트를 만들어서 이들 파일이
관련되어 있다는 것을 알려 주는 것이다. 컴파일러는 코드를 생성할 때, 프로
젝트에서 지정한 파일에 함수/변수가 있는지 검사한다. 이것이 실패하면 표준
라이브러리(standard library)에서 검사한다. 각각의 컴파일러에서 프로젝트를
만드는 법을 살펴보자. 설명을 간단히 하기 위해, 윈도우용 컴파일러에서는 콘
솔(console) 프로그램을 만드는 것을 예로 들었다.

  볼런드(Borland) C++ 3.1인 경우

  그림 9-1 Project->Open Project의 선택

  IDE4)의 메인 메뉴에서 Project->Open Project를 선택한다. 새로운 프로젝
  4) Integrated Develop Environment의 약자로 통합 개발 환경이라 한다.

트를 만들 때에도 Open Project를 선택해야 한다. 존재하는 프로젝트를 선택
하거나, 새로운 이름을 지정한다.

  그림 9-2 test.prj 프로젝트 이름의 지정

  우리가 만들고자 하는 프로젝트 이름은 main.prj이다. BC++ 3.1은 프로젝트
의 확장자로 .PRJ를 사용한다. 그림에 표시된 test.prj를 지우고 main.prj를 입
력한 다음, OK를 선택한다.

  그림 9-3 프로젝트에 sub.cpp와 main.cpp를 Add

  프로젝트 윈도우가 나타나면서 항목을 추가하라는 대화상자가 나타난다. 이
때 sub.cpp와 main.cpp를 추가한다. 후에 항목을 추가(add)하거나 삭제(delete)
할 때에는 Project 메뉴의 Add/Delete를 선택한다. 한 번 프로젝트가 선택되
면, 에디터 창에 있는 소스와는 상관없이 항상 프로젝트가 실행된다는 것을
주의하라.

  그림 9-4 모든 파일이 추가된 후의 Project 윈도우

  최종적으로 완성된 프로젝트 윈도우의 모양은 그림과 같다. n/a는 Not
Available의 약자로 그 항목이 사용 불가능함을 나타낸다.

  볼런드 C++ 4.5인 경우

  볼런드 C++는 윈도우용으로 5.0까지 나와 있다5). 이것은 강력한 윈도우 라
  5) 필자가 이 글을 쓰는 지금은 1998년 4월 8일이다.

이브러리인 Object Window Library(OWL)를 지원한다. 하지만, 여기서는 콘
솔 응용 프로그램을 위한 프로젝트를 만드는 것을 예로 든다.

  그림 9-5 Project->New project의 선택

  Project->New Project를 선택한다.

  그림 9-6 경로와 프로젝트 이름 main.ide의 지정

  경로와 프로젝트 이름 main.ide를 지정한다. 볼런드 C++ 4.5의 프로젝트 파
일의 확장자는 .IDE이다. 일반적으로 프로젝트는 하나의 디렉토리에 만든다.
Target Type으로 EasyWin을 선택한다. 이것은 콘솔 응용 프로그램을 생성한
다.

  그림 9-7 생성된 Project 윈도우

  모든 지정이 끝나면, 프로젝트 윈도우가 나타난다. 프로젝트 윈도우에 프로
젝트 이름과 동일한 main.cpp, main.def6)와 main.rc7)가 자동으로 추가된다.
  6) 프로그램 정의 파일(definition file)이다.

  7) 리소스 정의(resource script) 파일이다.

  그림 9-8 메뉴에서 Add node의 선택

  마우스 오른쪽 버튼을 프로젝트 윈도우에서 누르면, <그림 9-8>과 같은 메
뉴가 나타난다. Add node를 선택한다.

  그림 9-9 sub.cpp와 main.cpp의 Add

  대화상자에서 sub.cpp를 추가한다. main.cpp는 이미 지정되어 있으므로, 추
가할 필요가 없다.

  그림 9-10 sub.cpp와 main.cpp가 추가된 Project 윈도우

  .DEF 파일과 .RC 파일은 윈도우용 응용 프로그램에만 필요하므로, 콘솔 응
용 프로그램에서는 필요 없다. 그러므로 이 노드를 삭제해야 한다. 오른쪽 버
튼을 삭제를 원하는 노드 위에서 누르면, 아래 <그림 9-11>과 같은 메뉴가
나타난다.

  그림 9-11 main.def와 main.rc의 삭제

  Delete node를 선택해 필요 없는 main.def와 main.rc를 지운다.

  그림 9-12 완성된 Project 윈도우

  완성된 프로젝트 윈도우의 모양은 <그림 9-12>와 같다.

  볼런드 C++ 빌더(Builder) 1.0인 경우

  C++ 빌더는 비주얼 RAD(rapid application development) 툴(tool) 이다. 비
록 대부분이 비주얼한 프로그램을 만들기 위해 있는 것은 사실이지만, 콘솔
프로그램을 만드는 것을 지원한다. 실제로 이 책에서 그래픽을 사용하지 않는
대부분의 C++소스 프로그램은 C++빌더에서 테스트하였다. 그리고 몇몇 프로
그램은 C++빌더에서 동작하도록 설계되었다. 이 책에서 C++빌더에 대해서는
설명하지 않는다.

  그림 9-13 C++ 빌더의 메인 윈도우

  C++ 빌더의 메인 윈도우는 <그림 9-13>과 같다.

  그림 9-14 File->New의 선택

  File->New를 선택한다.

  그림 9-15 Console App의 선택

  나타나는 대화상자에서 Console App를 선택한다.

  그림 9-16 자동으로 생성된 project1.cpp

  환경에서 설정한 디렉토리에 자동으로 project1.mak8) 파일이 만들어진다.

  8) 메이크(make) 파일이다. 프로젝트를 컴파일하여 실행 파일을 만들기(make) 위한 잡

    다한 정보를 포함한 스크립트 파일이다. 윈도우용 프로그램의 프로젝트 파일은 대부분

    .MAK를 사용하며, 표준이므로 컴파일러들 사이에서 호환된다.

C++ 빌더의 프로젝트 파일의 확장자는 .MAK이다. 그리고 코드 윈도우에 자
동으로 생성된 project1.cpp파일이 나타난다.

  그림 9-17 View->Project Manager의 선택

  View->Project Manager를 선택하여, Project Manager를 표시한다.

  그림 9-18 Project Manager의 모양

  이미 추가된 project1.res와 project1.cpp가 보인다. sub.cpp를 추가해야 하므
로, 추가(Add) 아이콘을 선택한다.

  그림 9-19 sub.cpp를 추가

  대화 상자에서 sub.cpp를 추가한다. main.cpp는 추가하지 않는다. main.cpp
의 내용을 project1.cpp에 적는다. 만일 main.cpp를 그대로 사용하기를 원한다
면, File->Save project as를 선택해 프로젝트 이름을 main.mak로 정한다.

  그림 9-20 추가된 sub.cpp가 코드 윈도우에 나타난 모습

  추가된 sub.cpp가 코드 윈도우에 나타난다. 멀티 탭(multi-tab) 컨틀롤로 소
스 사이를 이동할 수 있다.

  그림 9-21 project1.cpp를 수정; #include"sub.h"의 추가

  project1.cpp에 #include "sub.h"문장을 추가한다. main의 소스 코드에 원하
는 작업을 코딩한다.

  마이크로소프트 비주얼(Visual) C++ 4.2인
경우

  VC++는 이 글을 쓰는 시점에 5.0까지 나와 있다. 5.0도 이와 비슷하게 프로
젝트를 만든다. VC++는 <그림 9-22>와 같다.

  그림 9-22 VC++ 4.2의 모습

  프로젝트가 이미 열려있다면, 프로젝트를 닫는다. 콘솔 프로그램을 만들기
위해서 프로젝트를 만들어야 한다. 소스 파일이 1개라도 윈도우용 프로그램은
기본적으로 필요한 파일이 여러 개이기 때문에 반드시 프로젝트를 만들고 시
작한다.

  그림 9-23 File->New의 선택

  File->New를 선택한다.

  그림 9-24 Project Workspace의 선택

  나타나는 대화상자에서 Project Workspace를 선택하고, OK를 선택한다.

  그림 9-25 Console Application의 선택

  프로젝트 타입을 묻는 대화상자에서 Console Application을 선택하고,
Name에 프로젝트의 이름을 정한다. 지금 예에서는 main이라고 정하였다. 확
장자는 .MAK이지만 정하지 않아도 된다. 이것은 C++ 빌더와 호환되는 윈도
우 표준 포맷의 텍스트 파일이다.

  그림 9-26 Insert->Files into Project의 선택

  필요한 파일을 추가하기 위해, Insert->Files into Project를 선택한다.

  그림 9-27 sub.cpp와 main.cpp의 Add

  파일을 선택하는 대화상자에서, sub.cpp와 main.cpp를 추가한다.

  그림 9-28 최종의 Project Workspace 윈도우

  VC++의 IDE에 프로젝트의 내용이 표시된다. 프로젝트는 Project
Workspace 윈도우에 나타난다.

  마이크로소프트 비주얼 C++ 5.0인 경우

  비주얼 C++ 5.0은 MFC 라이브러리는 그대로 4.2버전을 사용하고 있다. 하
지만, 새로운 C++의 키워드 - mutable, explicit, namespace등 - 를 지원하며,
몇 가지 툴(tool)들이 추가되었고, ATL(Active Template Library)의 지원 기
능이 강화되었다. 여기서는 Win32 Console Application의 프로젝트를 만드는
과정을 살펴보자.

  그림 9-29 File->New를 선택하면 프로젝트 타입을 결정할 수 있다.

  File 메뉴를 부른다. New를 선택하면 프로젝트의 타입을 결정하는 대화상
자가 실행된다.

  그림 9-30 Project 탭에서 Win32 Console Application을 선택하고, Project Name 글상자

    에 test를 입력한다.

  여러 가지 다양한 프로젝트 타입을 선택할 수 있는데, 이 책에서 사용하는
대부분의 예는 Win32 Console Application을 선택해야 한다. 이것은 옛날 콘
솔 프로그램 - 예를 들면 도스창 같은 - 를 흉내(emulate)낸다. 프로젝트 이
름을 입력하는 글상자(text box)에 test를 입력한다. Location에 설정된 폴더에
test라는 폴더가 만들어지면서, 필요한 파일들이 자동으로 생성된다.

  그림 9-31 Workspace 윈도우의 File View탭에는 아직 어떠한 파일도 추가되지 않았다.

  Workspace윈도우에 프로젝트의 골격이 형성되었다. 아직 아무런 파일도 추
가되지 않은 상태이므로, Class View탭에는 어떠한 클래스도 보이지 않는다.
물론 소스에 클래스가 없다면, 파일이 추가되어야 클래스는 보이지 않을 것이
다.

  그림 9-32 메뉴의 Project->Add To Project->New를 선택하면, 새로운 소스 파일을 만들

    수 있다.

  이제 필요한 파일 - sub.cpp와 main.cpp - 을 프로젝트에 추가한다. Project
메뉴의 Add To Project의 하위 항목을 선택한다. 새로운 파일을 만들거나 이
미 존재하는 파일을 프로젝트에 추가하는 것이 가능하다.

  그림 9-33 New 대화상자의 Files 탭에서 C++ Source File을 선택하고 File 글상자에 sub

    를 입력한다.

  Add To Project->New를 선택했을 때 나타나는 대화상자에서 C++ Source
File을 선택하고, File 이름을 입력하는 글 상자에 sub.cpp를 입력한다. 확장자
는 입력하지 않아도 된다.

  그림 9-34 다시 Project->Add To Project->New를 선택해 sub.h와 main.cpp를 모두 추가

    하여야 한다9).

  9) 지금의 예는 New를 선택해 파일을 모두 새로 만드는 경우이지만, 소스 파일이 이미

    존재하는 경우는 Files를 선택해서 추가한다.

  다시 Add To Project->New를 선택해 sub.h를 만들고, main.cpp를 프로젝
트에 추가해야 한다.

  그림 9-35 이번에는 C/C++ Header File을 선택하고 File 글상자에 sub.h를 입력한다. 물

    론 확장자 .h는 생략 가능하다.

  그림 9-36 Workspace 윈도우에 이제 모든 파일 - main.cpp, sub.cpp - 이 추가되었다

    10).

  10) 그림에는 sub.h가 보이지만 실제로 이 파일은 프로젝트에 포함하지 않아도 된다.

    지금의 경우는 파일이 디스크에 없으므로 만들기 위해서 추가된 것이다. 만들고 나서

    는 프로젝트에서 삭제한다.

  Workspace 윈도우에 모든 파일이 추가되었다. sub.h는 파일을 만들기 위해
서 추가하였으므로, 프로젝트가 모두 완성된 후에는 *.h 파일은 프로젝트에서
제거해도 된다.

  연습문제

1. Borland C++ 5.0에서 프로젝트를 만드는 과정을 기술하라.

2. C++ Builder 3.0에서 프로젝트를 만드는 과정을 기술하라.

3. Visual C++ 98에서 프로젝트를 만드는 과정을 기술하라.


10 변수의 종류에 따른 범위 규칙

10. 변수의 종류에 따른
범위 규칙(scope rule)

  변수의 종류에 따라 변수의 적용 범위와, 메모리에 할당되고 해제되는 시기
가 결정되므로 변수의 종류를 이해하는 것은 프로그램의 논리적인 부분을 이
해하는데 매우 중요하다. 변수의 종류를 구분하는 방법은 2가지가 있다. '메모
리의 어디에 기억되느냐'로 구분하는 기억 부류(storage class)에 의한 구분
방법과 '변수가 어느 범위에서 사용 가능하냐'로 구분하는 통용 범위(visible
scope)1)에 의한 구분이 그것이다.

  1) 이것은 가시성(visibility)이라고도 한다. 이것은 변수가 선언된 시점에서 어느 곳에서

    이 변수를 볼 수 있느냐의 의미이다.

  먼저 기억 부류에 의한 구분을 알아보자. 기억 부류의 지정은 기억 부류
지정자(storage class specifier)에 의해 이루어진다. 기억 부류 지정자는
auto, static, register, extern과 mutable이 있다. 이것은 각각 메인 메모리의
스택(stack), 메인 메모리의 힙(heap), CPU의 레지스터(register)와 하드디
스크(hard disk)의 파일 영역에 할당된 변수를 의미한다. mutable은 C++에 새
로 추가된 사항으로 '생성자,소멸자'에서 다룬다.

    ■ auto

    ■ static

    ■ register

    ■ extern

    ■ mutable

  그림 10-1 기억 부류: 각각의 변수는 고유의 기억 장치에 할당된다. 스택, 힙, 레지스터,

    디스크는 모두 메모리이다.

  스택(stack)

  변수 이름 앞에 붙은 auto는 이 변수가 메모리의 스택에 할당됨을 나타낸
다. 스택에 할당되는 변수는 컴파일러에 의해서, 자동으로 할당과 해제를 위한
코드가 생성되므로 자동 변수(auto variable)라 한다.

#include <stdio.h>

void f(int k) {

    int i=3,j=4;

    printf("%d,%d,%d\n",i,j,k);

}//f

void main() {

    int i;

    i=2;

    f(i);

}//main

  위의 예에서 main()함수 안에서 사용하는 변수 i는 main()의 블락을 처음
접근할 때, 스택에 할당된다. 이 변수는 main()안에서만 사용하고 밖에서는 필
요 없는 변수이므로 스택에 할당되는 것이 바람직하다. 이러한 변수의 주소는
컴파일 시간에 결정되므로 변수 선언문 int i;가 실제로 실행되는 것은 아니다
2). 스택에 할당된 변수 i는 main()함수를 탈출하기 전, 스택 메모리에서 해

  2) 컴파일러의 트레이스(trace) 기능을 이용하여 추적해 보라. 변수 선언문은 실행되지

    않는다. 단지, 실행 코드를 생성할 때, 변수의 적절한 스택에서의 상대 주소를 결정짓

    는 역할을 해 준다.

제(free)된다. 메모리 할당과 해제에 관한 코드는 사용자의 의도와는 상관없이
자동으로 진행된다3).

  3) 즉 컴파일러가 이러한 코드를 생성한다.

  main()에서는 함수 f()를 호출하고 있는데, 함수 f()의 파라미터 k나 내부에
서 선언된 변수 i,j 역시 함수 f()가 종료되었을 때는 스택에서 해제된다. 함수
의 내부에서 사용하는 변수의 수를 어떻게 효율적으로 관리할 것인가? 바로
스택을 사용하는 것이 그 열쇠이다. 스택에 관한 설명은 '&가 있어야 하는
가?'를 읽어 보라. 변수가 스택에 할당된다는 것을 알려 주기 위해, auto를 변
수의 형 이름 앞에 붙일 수 있다. 위의 소스는 아래와 대등하다.

#include <stdio.h>

void f(auto int k) {

    auto int i=3,j=4;

    printf("%d,%d,%d\n",i,j,k);

}//f

void main() {

    auto int i=2;

    f(i);

}//main

  블락 안에서 선언된 변수는 기본으로 자동 변수로 간주된다. 즉 auto를 명
시하지 않아도 있는 것으로 간주한다. 여러분이 프로그램을 코딩할 때 명시적
으로 auto를 붙이지 않아도 좋으며, 이것이 일반적이다.

  매번 스택에 할당되는 변수들은 초기화를 하지 않는 것이 속도가 빠를 것이
다. 그래서 자동 변수들은 초기화가 이루어지지 않는다. 초기화가 안된 자동
변수 값은 얼마인지 모르는 값 - 할당된 메모리 위치에 있던 원래 값, 즉 쓰
레기 값(garbage value) - 을 가진다.

  힙(heap)

  변수의 종속 기간이 블락이 아니라, 프로그램이 시작하여 종료할 때까지라
면, 이러한 변수는 스택보다는 힙(heap)에 할당되는 것이 바람직하다. 힙은
프로그램 코드와 스택 사이의 빈(empty) 큰 메모리 덩어리(heap)를 말한다.
힙은 일반적으로 동적 메모리 할당(dynamic memory allocation)시에 할당되
지만, static이란 변경자를 붙인 변수들은 힙에 할당된다. 이러한 변수들의 메
모리 주소는 프로그램이 시작하여 종료할 때까지 변하지 않는다. 그래서 정적
(static)이란 말은 적절하다. 또한 프로그램이 시작할 때 1번만 할당이 되므로
초기화가 속도를 느리게 만드는 것은 아니다. 그래서 정적 변수(static
variable)는 프로그램이 시작할 때, 0으로 초기화가 일어난다. 아래의 예를 참
고하라.

#include <stdio.h>

int i;

void main() {

    printf("%d\n",i);

    //       0

}//main

  위의 예에서 함수 밖 - 즉 파일 안4) - 에서 선언된 변수 i는 정적 변수이

  4) 파일 안에서 선언된 변수는 그 파일에 있는 모든 함수가 그 변수를 참조할 수 있으

    므로, 파일 범위(file scope)를 가진다고 한다. 반면에 블락 안에서 선언된 변수는 블

    락 범위(block scope)를 가진다. 이 밖에도 프로토타입 범위(prototype scope)를 가

    지는 변수처럼 보이는 식별자(identifier)도 있다.

다. 명시적인 초기화를 해 주지 않았지만, 0으로 초기화된다. 명시적으로
static를 붙여도 되지만, 붙이지 않는 것이 일반적이다5).

  5) 실제로 전역 변수를 static이라고 선언하는 것과, 그렇지 않은 것은 다르다. 하지만,

    차이가 근소하므로(?) 같다고 외워도 무방하다.

#include <stdio.h>

static int i;

void main() {

    printf("%d\n",i);

}//main

  독자들은 정적 변수 i를 1로 초기화시키기를 원할지도 모른다. 그렇다면,

static int i=1;

이란 문장을 사용하면 된다.

  블락 안에서 선언된 변수는 auto가 디폴트(default)이고, 파일 안에서 선언
된 변수는 static이 디폴트(default)인 것을 이해했을 것이다. 하지만, 블락 안
에서 선언된 변수가 항상 auto인 것은 아니다. 즉 블락 범위(block scope)를
가지면서, 정적(static)인 변수가 - 일반적인 것은 아니지만 - 존재한다.

  필자가 처음 C언어를 배울 때, strtok6)()의 신비한 기능에 놀란 적이 있

  6) String To Token의 약자를 사용한 함수로, 표준함수이다. string.h에 선언되어 있다.

    문자열에서 구분자(delimiter)로 구분되는 토큰을 구한다.

다. 함수가 종료하고 나서도 함수의 상태를 어떻게 유지하는가? 이것은 C++
의 클래스를 사용하면, 사소한(trivial) 것이다. 하지만, C에서 함수의 상태를
유지하는 유일한 방법은 정적 변수를 사용하는 것이다7).

  7) 함수의 상태를 유지하기 위해서 전역 변수를 사용하는 것은 큰 위험이다. 이것은 가

    능하지만 좋은 방법은 아니다.

#include <stdio.h>

void f() {

    int i=0;

    printf("%d\n",i);

    ++i;

}//f

void main() {

    f();

    f();

    f();

}//main

  위 프로그램의 출력 결과는

0

0

0

이다. 비록 f()의 마지막에서  ++i를 하더라도, 이것은 자동 변수이므로, 매번
할당과 해제가 반복된다. 그러므로 함수 f()를 빠져 나오는 순간 스택에서 해
제될 것이며, 이 값은 사라질 것이다. 또한 함수 f()가 호출될 때마다 0으로
초기화가 일어날 것이다. 하지만, f()의 변수 i를 정적(static)으로 선언하면, 이
것은 힙에 유지되므로 함수가 호출될 때마다, 초기화가 되지 않는다.

#include <stdio.h>

void f() {

    static int i;//이 문장은 컴파일 시간에 단 한 번 실행된다.

    printf("%d\n",i);

    ++i;

}//f

void main() {

    f();

    f();

    f();

}//main

  프로그램의 출력 결과는

0

1

2

이다. 위에서 f()의 정적 변수 i는 초기화하지 않았지만, 초기화가 있다고 해서
이것을 매 번의 초기화로 간주해서는 안 된다. 즉,

static int i=3;

이라는 문장이 f()에서 쓰였다면, 이것은 함수 f()가 호출될 때마다 3으로 초기
화하라는 문장이 아니라, 프로그램 시작 시에 i를 3으로 초기화하라는 의미이
다.

#include <stdio.h>

void f() {

    static int i=3;

    printf("%d\n",i);

    ++i;

}//f

void main() {

    f();

    f();

    f();

}//main

  그러므로, 위 프로그램의 출력 결과는

3

4

5

이다. 여러분은 이제

int i=3;

과

int i;

i=3;

이 어떻게 다른지 알 수 있을 것이다.

#include <stdio.h>

void f() {

    static int i;

    i=3;//이 문장은 매번 실행된다.

    printf("%d\n",i);

    ++i;

}//f

void main() {

    f();

    f();

    f();

}//main

위 프로그램의 출력 결과는

3

3

3

이다. 물론 자동 변수라면 int i=3; 과 int i; i=3; 은 동일하다.

  컴파일러가 실행 코드를 생성할 때, 정적 변수의 위치는 알려진다. 그러므
로, 변수를 초기화하거나 선언하는 문장은 실제로 실행되기 전에 만들어져 있
다. 그러므로 변수를 선언하는 문장은 실행 시에 실행되는 것이 아니다8).

  8) 실제로 변수의 선언문이 실행되지 않고 변수가 할당될 수는 없다. 소스의 변수 선언

    문은 컴파일러가 적당한 스택 혹은 힙의 주소를 계산하여, 이것을 각 변수 자리에 넣

    음으로써 구현된다. 즉 int i; 라는 문장은 아마도 스택의 첫 부분에 할당될 것이다. 그

    러므로 i++; 라는 문장이 그 이후에 쓰였다면, 스택의 첫 부분에 있는 정수를 1증가시

    키는 코드를 생성할 것이다. 그러므로 실행 시에는 변수의 선언문을 보고 스택 혹은

    힙에 변수를 할당하는 것이 아니라, 이미 그 만큼의 변수를 할당하는 코드가 생성되어

    져 있으므로, 선언문은 실행되지 않는 것이다.

독자들은 이것을 확인해 보라. 각 컴파일러의 디버그(debug) 기능을 이용하여
위의 소스 코드를 추적(trace)해 보라. 변수 선언문은 실행되지 않을 것이다.
static 변수를 이용하면, 함수의 상태를 유지하도록 만들 수 있다. 물론 이것은
전역 변수로도 가능하지만, f()만이 사용하는 상태를 외부에 두는 것은 바람직
하지 않다. strtok()는 이렇게 동작한다. 이 함수의 자세한 사용법은 컴파일러
의 도움말에서 제공하는 예제를 참고하라.

  함수의 상태를 유지하기 위해 static 변수를 사용하는 것은 C에서 중요한
것이다. 하지만, C++에서 클래스를 사용하면, 이것은 매우 사소하므로, 이것으
로 static 변수에 대한 이야기는 끝맺고자 한다.

  레지스터(register)

  독자들은

++i;

라는 문장이 메모리를 몇 번 접근(access)하는지 알고 있는가? 단지 i를 1증가
시키는 이 문장은 i 값을 읽기(read) 위해 한 번, 증가된 i값을 쓰는데(write)
한 번 접근한다. 모든 변수는 메모리에 있으므로 컴퓨터가 이 값을 증가시키
기 위해서는 먼저 이 값을 CPU로 레지스터로 읽어와야 한다. 그리고 명령어
코드(op-code) 부분을 해독하여 이 값을 증가시키는 것을 파악한 다음, 레지
스터의 값을 증가시킬 것이다. 그리고 증가된 이 값을 다시 메모리의 원래 위
치에 쓸 것이다9). 아래의 <그림 10-2>를 참고하라.

  9) 이것은 기계 사이클(machine cycle)로 알려져 있다. 대부분의 명령은 다음과 같은

    순서로 수행된다. 명령어 가지고 오기(fetch) → 명령어의 피연산자(operand) 가지고 오

    기(load) → 명령 실행(execute) → 결과 저장(store). 기계 사이클 중 가장 많은 시간을

    차지하는 부분은 load와 store일 것이다. 이것은 메모리의 성능에 따라 다르지만, 일반

    적으로 CPU의 레지스터에서 작업하는 것에 비해 수 십배 이상 느리다.

  그림 10-2 기계 사이클(machine cycle): 메모리의 한 변수를 갱신하기 위해서는 두 번

    메모리에 접근해야 한다.

  만약 변수가 메모리에 할당되지 않고 CPU의 레지스터에 할당된다면, 이러
한 메모리 참조의 오버헤드는 없을 것이다. 그리고 레지스터의 값이 바로 갱
신되기 때문에, 메모리에 선언된 변수에 비해 수 십 배 이상 빠르게 처리될
것이다. 이처럼 변수를 CPU의 레지스터에 할당하기 위해서는 register라는
키워드를 사용한다.

#include <stdio.h>

void main() {

    register int ip;

    ip=2;

    printf("%d\n",ip);

}//main

  두 개 이상의 기억 부류 지정자를 사용할 수는 없다. 그러므로

auto register int i;//error

는 잘못이다. register는 반드시 레지스터에 변수가 할당된다는 보장이 없다.
만약 프로그램이 수행중인 시점에서, CPU에 남는 레지스터가 있다면, 그렇게
될 것이다. 하지만, 남는 레지스터가 없다면, 스택에 할당이 된다. 또한
register가 아닌 auto변수도 레지스터에 할당이 되는 경우가 있다. 대부분의
컴파일러는 최적화(optimization)를 수행하는데, 속도를 증가시키기 위해 스
택 변수도 레지스터 변수로 - 가능하다면 - 전환할 것이다. 이것은 볼런드와
마이크로소프트 모두의 컴파일러에서 그러하다.

  이런 응용은 어떤가? 만약 메모리의 특정 영역이 특정 보조 장치의 상태를
나타내기 위해 사용되었다면, 독자들은 이 부분을 읽는 코드를 작성할 것이다.
하지만, 반드시 이 부분이 가장 최근의 값으로 읽힌다는 보장이 없을 수도 있
다. 컴파일러의 최적화 코드는 가장 최근에 읽은 이 부분의 내용이 아직 레지
스터에 남아 있다면, 이 부분을 읽지 않을 것이다. 하지만, 메모리의 내용은
보조 장치에 의해 갱신되었을 수도 있다. 이러한 경우는 변수를 참조하기 위
해 무조건 메모리의 내용을 읽으라고 지시해야 할 것이다. 이것은 volatile 키
워드를 사용한다. volatile은 반드시 그 변수의 메모리 값이 읽힌다는 보장을
하는 코드를 생성한다. 아래의 예제를 보자.

volatile int ticks;

void timer( ) {

   ticks++;

}

void wait (int interval) {

   ticks = 0;

   while (ticks < interval);  // Do nothing

}

  위의 예제에서는 timer()가 타이머 하드웨어에 의해서 불리는 배경 작업이
라고 간주했다. 그러므로 타이머 틱(tick)을 가지는 ticks는 최적화의 대상이
되면 안되므로, volatile을 사용하여 선언하였다. 그러므로 volatile은 인터럽트
루틴 혹은 I/O의 배경 루틴이 사용하는 전역 변수인 경우 대부분 붙여준다.

  독자들은 Win95 프로그래밍에서 쓰레드(thread) 프로그래밍의 경험이 있
을지도 모른다. 쓰레드 동기화와 통신을 위해서 사용하는 변수는 모두
volatile로 선언한다. 이것 역시 같은 이유이다. 쓰레드 프로그래밍은 해당 서
적을 참고하라.

  파일(file)

  마지막으로 살펴 볼 변수의 기억 부류는, 이 변수를 가리키는 코드가 미리
컴파일 되어서 디스크에 다른 파일로 존재하는 경우에 발생한다. 즉 하나의
프로그램이 여러 개의 소스 코드로 분리되어 작성된 경우에 이러한 extern 선
언이 필요해 진다. 예를 들어 성적 처리를 위한 프로그램이 3개의 소스 파일
로 나뉘어 개발된다고 가정하자. 각각은 성적을 입력받는 input.cpp, 성적 처
리를 하는 main.cpp와 성적 처리를 하는 output.cpp이다. main.cpp는
input.cpp의 함수들과 output.cpp의 함수들을 사용하며, main()함수를 가지고
있다. 학생의 전체 수가 input.cpp에서 아래처럼 전역 변수에 관리된다고 하
자.

//input.cpp

...

int nTotalStudent;//이 변수에 학생 전체 수가 관리된다.

...

void ReadDataFromFile() {

    ...

}

...

  input.cpp의 변수 nTotalStudent는 아마도 main.cpp와 output.cpp에서 모두
사용될 것이다. 그렇다면, main.cpp에서 input.cpp에서 사용하는 변수
nTotalStudent를 어떻게 선언하는가? 해답은 바로 extern이다. 독자들은
extern 없이 아래와 같은 문장이 main.cpp에서 사용된다면 가능하다고 생각할
지 모른다.

//main.cpp

int nTotalStudent;

...

  하지만 위의 소스 코드는 main.cpp에서 사용 가능한 또 하나의 전역변수
nTotalStudent를 선언하는 문장이다. 그러므로 nTotalStudent가 이미 선언되
어 있는 변수라는 변경자를 붙여주어야 하는 것이다. 이것이 extern이다.
main.cpp는 아래와 같이 작성되어야 한다.

//main.cpp

extern int nTotalStudent;

...

  그림 10-3 extern은 변수가 다른 파일에 이미 존재한다는 선언이다.

  함수인 경우도 마찬가지이다. 하지만, 함수인 경우 extern은 선택사항이다.
그러므로 명시적인 extern 지정이 필요하지 않다. 즉 독자들은 다른 파일에서
정의된 함수를 선언하기 위해 extern을 붙이지 않아도 된다. 이것은 stdio.h에
쓰여진 printf()의 선언에 extern이 붙지 않은 이유이다.

  독자들은 어떤 파일에서 선언된 전역 변수가 반드시 다른 파일에서 볼 수
있는 것이 아니다는 - extern을 사용하더라도 - 것을 주목해야 한다. 만약
input.cpp에서 nTotalStudent가 static 변경자로 선언되었다면,

static int nTotalStudent;

이 변수는 input.cpp에서만 볼 수 있다. 즉 main.cpp에서 이 변수를 참조하기
위해,

extern static int nTotalStudent;

라고 선언하면, 컴파일 시간 에러가 발생한다. 그러므로 static이 붙은 전역 변
수는 파일 범위(file scope)를 가진다고 한다. 하지만 static이 붙지 않은 전
역 변수는 전역 범위(global scope)를 가진다. extern을 사용한 실행 가능한
예제는 '프로젝트 만들기'를 참고하라.

  extern 키워드와 관계된 다른 사항들을 살펴보자. 이것은 C언어로 컴파일된
함수를 C++에서 사용하려고 하는 경우 발생한다. C와 C++가 함수의 실행코
드(execution code)를 생성하는 방식은 조금 다르다. C++에서 함수 오버로딩
을 어떻게 구현하는가? C++는 유일한(unique) 함수를 호출하기 위해 오버로
딩된 함수들에 대해 이름 바꾸기(name mangling)를 수행한다. 이것은 같은
이름의 함수에 대해 이들을 구분하기 위해 C++ 컴파일러에 의한 자동화 과정
이다. 만약 test(int)라는 함수와 test(long)이라는 함수가 있다면, 컴파일러는
아마도 각각의 함수를 testA(int)와 testB(long)라는 독립된 함수로 이름을 바
꿀 것이다. 또한 C++에 새로운 reference와 this라는 포인터를 관리하기 위해
부득불 C의 방식과는 다른 코드 생성이 필요했을 것이다.

  그렇다면 이미 만들어진 수많은 C코드를 사용할 수 없다는 말인가? 그렇지
않다. 바로 extern "C"가 그 열쇠다. C++에서 C의 함수를 호출하기 위해서
선언할 때, C 함수의 헤더 앞에 extern "C"를 붙여주어야 한다.

extern "C" void cfunc(int);

  위 문장은 cfunc()라는 C 함수를 C++에서 사용하기 위해 선언하고 있는 문
장이다. 이러한 C함수가 굉장히 많다면, 함수마다 extern "C"를 붙여주는 것
은 번거로운 일이다. 이 때에는 extern "C" { ... } 블락안에 C함수의 선언을
적으면 된다.

extern "C10)" {

  10) 소문자 c를 적어도 무방하다. 즉 extern "C"와 extern "c"는 둘 모두 타당하다.

    void cfunc(int);

    ...

}

  가시범위(visibility)에 의한 구분

  변수가 선언되었을 때, 이 변수를 어디에서 사용할 수 있는가? 또한 어디에
서 사용할 수 없는가? 이러한 규칙은 범위 규칙(scope rule)이라고 알려져
있으며, 특정 변수가 그 범위에서 사용 가능하면, 볼(visible) 수 있다11)고

  11) '비지블하다' 혹은 '보인다'고 말한다.

한다. 변수는 다음과 같은 5가지 범위(scope)를 가질 수 있다12).

  12) C++이 클래스 범위(class scope)라는 새로운 변수의 범위를 형성하는 것은 사실이

    다. 이렇게 클래스 범위 안에서 사용 가능한 변수는 변수(variable)라고 부르지 않고, 멤

    버 변수(member variable) 혹은 데이터 멤버(data member)라고 부른다. 하지만, 클

    래스도 하나의 블락을 형성하므로 여기서는 블락 범위에 멤버 변수를 포함하도록 하겠

    다. 또한 멤버 변수는 다양한 접근 지정자(access specifier)에 의해 가시 범위가 바뀌

    므로 클래스를 설명할 때 자세하게 다룬다.

  ① 블락 범위(block scope)		- 지역 변수(local variable)

  ② 파일 범위(file scope)		- 전역 변수(global variable)

  ③ 전체 범위(global scope)		- 전역 변수(global variable)

  ④ 프로토타입 범위(prototype scope)

  ⑤ 이름공간 범위(namespace scope)

  블락 범위를 가지는 변수를 지역 변수(local variable)라 하며, 파일 범위
와 전체 범위를 가지는 변수를 전역 변수(global variable)라 한다. 프로토타
입을 가지는 심벌은 변수로서 사용되지 않으나, 범위가 구분되는 것은 사실이
다.

  이름공간 범위는 '이름공간'에서 별도로 다룬다.

  블락 범위(block scope)

  독자들은 블락의 특징을 알고 있을 것이다. 블락은 여는 중괄호(open brace:
{)와 닫는 중괄호(close brace: })로 표현되며 다음과 같은 특징을 가진다.

  ① 변수 선언을 임의의 위치에 가질 수 있다.

  ② 하나의 문장 취급된다. 그러므로, 겹쳐짐(nesting)이 가능하다. 즉, 블락

     안에 또 다른 블락이 올 수 있다.

  블락은 변수 선언을 가질 수 있는데, 이처럼 블락 안에서 선언된 변수를 지
역 변수라 하며, 이러한 변수는 그 변수가 선언된 블락 안에서만 사용할 수
있다. 아래의 예제를 참고하라.

#include <stdio.h>

void main() {				//main 블락

    int i=1,k=100;

    {					//1번 블락

        int j=2;

        {				//2번 블락

            int i=3;

            printf("%d,%d\n",i,j);

        }

        printf("%d,%d\n",i,j);

        int i=4;

        printf("%d,%d,%d\n",i,j,k);

    }

    printf("%d\n",i);

}//main

  먼저 2번 블락의 printf()를 보자. i는 선언되어 있으며 값은 3이다. j는 2번
블락에서 선언되어 있는 것은 아니지만, 1번 블락에 선언되어 있으며 값은 2
이다. 2번 블락은 1번 블락에 속한 하나의 문장 취급되므로, 2번 블락에서 1번
블락이나 main 블락의 변수를 볼 수 있다는 것에 주의하라. 그렇다면, 2번 블
락에서 k를 볼 수 있는가? 그렇다. 그렇다면 2번 블락에서 사용된 다음 문장
은 100을 출력할 것이다.

printf("%d\n",k);

  1번 블락에서는 2개의 지역 변수가 선언되었지만 선언된 위치는 다르다. 첫
번째 printf()는 i가 선언되기 전이므로, main 블락의 i를 출력한다. j는 2가 출
력될 것이다. 1번 블락의 두 번째 printf()에서는 i와 j는 1번 블락의 변수이지
만, k는 1번 블락에 없으므로 main 블락을 찾을 것이며, 이것은 100이다.

  main 블락의 printf()는 i가 main 블락에 선언되어 있으므로, 1이 출력될 것
이다. 출력 결과는 아래와 같다.

    3,2

    1,2

    4,2,100

    1

이다. 지역 변수 - 블락 안에서 선언된 변수 - 의 범위 규칙(scope rule)은 다
음과 같다.

"사용된 변수는 자신을 둘러싼 가장 가까운 블락의 변수가 사용된다. 변수
이름이 같은 것은 1개의 블락에 같이 존재할 수는 없지만, 다른 블락에는
존재할 수 있다. 이러한 경우는 변수가 사용된 시점에서 둘러싼 가까운
블락의 변수가 사용된다. 변수를 둘러싼 어떠한 블락에도 그러한 변수의
선언이 없다면 에러이다."

  파일 범위(file scope)

  전역 변수는 블락 안에서 선언되지 않은 변수를 말한다. 그러므로 함수의
밖에서 선언될 것이다. 이러한 변수는 그 변수가 선언된 시점의 아래에 있는
모든 함수들이 그 변수를 볼(visible) 수 있다. 즉 파일 하나를 한 개의 블락으
로 취급한 것과 같다. 그러므로, 전역 변수를 파일 범위(file scope)를 가진다
고 하지만, 항상 그런 것은 아니다. 아래의 예제를 참고하라.

#include <stdio.h>

int i=1;

void f(int j) {

    printf("%d,%d\n",i,j);

}

int j=2;

void g() {

    printf("%d,%d\n",i,j);

}

void main() {

    int i=3;

    f(i);

    g();

}//main

  전역 변수 i는 f(), g()와 main()에서 모두 볼 수 있다. 하지만, 전역 변수 j
는 f()이후에 선언되었으므로, g()와 main()에서만 볼 수 있다. 지역 변수와 전
역 변수가 모두 보이는 경우는 어떻게 할 것인가? 이 때의 규칙은 아래와 같
다.

"같은 이름의 지역 변수와 전역 변수가 존재하는 경우, 가까운 쪽, 즉 지역
변수를 사용한다."

  이 규칙에 의하면, f()에서 j는 3이다13). 하지만, g()에서는 지역 변수가

  13) 파라미터로 넘어온 변수는 스택에 푸시되며, 또한 지역 변수이다.

없으므로 i와 j는 각각 전역 변수의 값인 1,2일 것이다. 또한 main()에서는 같
은 이름의 지역 변수 i가 존재하므로, i는 3이겠지만, j는 2일 것이다.

  프로그램의 출력 결과는

    1,3

    1,2

이다.

  독자들은 C++에서 이러한 변수의 범위 규칙을 위반하는 연산자를 배우게
될 것이다. 이것은 범위를 해결한다는 의미에서 범위 해결사(scope
resolver)라 한다. 기호는

::

를 사용한다. 아래의 예제를 참고하라.

#include <stdio.h>

int i=1,j=2;

void f(int j) {

    printf("%d,%d\n",i,::j);

}

void main() {

    int i=3;

    f(i);

}//main

  f()에서 ::j는 전역 변수 j를 의미하므로, 출력 결과는

1,2

이다14).

  14) 범위 해결사는 항상 전역 변수를 참조한다. 상위 블락의 변수를 참조하지 않는 것

    에 유의하라. 즉 아래의 문장은 에러이다.

      ... { int j=1; { int k=2; printf("%d,%d\n", k, ::j); } } ...

  위의 예에서 모든 전역 변수는 전역 범위(global scope)를 가진다. 그러므
로 이 변수를 다른 파일에서 extern을 사용해 참조할 수 있다. 만약 이러한
변수가 파일 범위(file scope)를 가지게 하려면, static 변경자를 붙여주면 된
다.

  프로토타입 범위(prototype scope)

  독자들은 함수도 쓰기 전에 선언되어야 한다는 것을 알고 있을 것이다. 아
래의 예제를 보자.

#include <stdio.h>

int f(int i,int j);//함수도 먼저 선언되어야 한다.

void main() {

    printf("%d\n",f(2,3));

}//main

int f(int i,int j) {

    return i*j;

}

  f()는 main()위에 선언되었다. 함수 f()의 선언은 함수의 이름과 파라미터의
형과 개수를 검사하므로, i와 j라는 명칭은 더미(dummy)이다. 즉 생략해도 된
다.

int f(int, int);

  이것은 i와 j는 컴파일러가 무시함을 의미한다. 그러므로 다음과 같은 선언
도 타당하다.

int f(int k, int l);

이때 k나 l등은 프로토타입 범위(prototype scope)를 가진다. 그러므로 함수
의 선언 범위에서만 서로 다른 명칭을 유지하면 된다.

  진보된 주제: static에 관한 진실

  위에서 설명한 static에 관한 설명은 초보자에게 설명하기 적당하게 조작된
것이다. 위의 설명대로 static을 이해해도 크게 무리는 없지만, 실제로 설명은
날조(fabrication)된 것이다. 이하의 설명은 초보자에게는 조금 어려운 진보된
주제이다. 이해가 되지 않는다면 건너뛰어도 좋다.

  다음과 같은 2개의 소스파일 sub.cpp와 main.cpp를 고려해 보자.

   sub.cpp

int i=1;

static int j=2;

...

   main.cpp

extern int i;

void f() {

    static int k=3;

    ...

}

void main() {

    int* ip;

    ip=new int[2];

    ip[0]=11;

    ip[1]=21;

    ...

    delete[] ip;

}

  sub.cpp는 2개의 전역 변수 i와 j를 각각 1, 2로 초기화하는 코드를 포함한
다. 단, 2번째 전역 변수 j에만 static 변경자를 붙였다.

  main.cpp는 sub.cpp의 전역 변수 i를 참조하는 코드 extern int i; 를 포함한
다. 함수 f()에서는 static변수 k를 3으로 초기화한다.

  함수 main()에서는 지역 변수 ip를 선언하여, C++의 동적 메모리 할당연산
자 - "new,delete연산자"에서 설명 - 를 사용하여, 동적으로 메모리를 할당
하고 있다. 실제로 이 코드는 어떻게 컴파일되어 실행코드가 만들어질까?

  실제로 컴파일러가 생성한 EXE 파일은 여러 개의 의미있는 블락들로 구성
되어 있다. 정확한 것은 아니지만■, 실행 코드를 포함하는 코드 블락(code

             전역 객체는 어디에 할당되는가? 전역 객체의 생성자는 main()함수보다
          먼저 실행된다. 전역 변수에 문자열 "hello"를 대입하는 문장이 있다면,
          "hello"는 어디에 할당될까? 실행 코드는 운영체제에 의존적으로 역할이 구
          분된 최소한 2개 이상의 블락으로 구성된다.

block)과 데이터가 들어 있는 데이터 블락(data block)으로 구성된다. 전역
변수는 모두 데이터 블락에 할당된다. 그러므로 sub.cpp의 i와 j는 데이터 블
락에 할당된다. 각각의 주소를 [1000], [1002]라 하자.

  또한 지역 변수중 static 변경자가 붙은 것 역시 데이터 블락에 할당된다.
그러므로 f()의 지역 변수 k는 데이터 블락에 할당된다. 이의 주소를 [1004]라
하자.

  그러면 static은 어떤 역할을 하는가? 바로 범위(scope)를 제한하는 역할을
한다. sub.cpp에 선언된 전역 변수 i는 static 변경자가 없으므로, main.cpp에
서 extern int i; 처럼 선언하여 이 변수를 참고할 수 있다. 즉, main.cpp에서 i
는 sub.cpp의 i, 즉, [1000]에 있는 i를 의미한다. sub.cpp에 있는 두 번째 변수
j는 static 변경자가 있으므로, 이 전역 변수를 다른 파일에서 참고할 수 없다.
만약 main2.cpp 에서 extern int j; 라고 선언하여 이 변수를 참조하려하면, 컴
파일 시간 에러가 발생한다.

  main.cpp의 함수 f()에 있는 지역 변수 k는 어떤가? 이 변수의 범위 역시
블락 안으로 제한된다. 즉 함수 f() 블락 외부에서 정적 변수 k를 참조할 수
없다. 결론적으로 말하면,

"static 변수는 힙에 할당되는 것이 아니라, 범위를 제한하는 일을 한다."

  그러면 힙(heap)에는 어떤 변수가 할당되는 것일까? 위의 코드가 실행되는
과정을 따라가며, 각각의 변수가 어떻게 유지되는지 살펴보자.

            실제로 static변수는 힙(heap)에 할당되지 않는다. 힙은 new와 같은 동적 메
          모리 할당 연산자, 혹은 malloc()같은 동적 메모리 할당 함수에 의해서 할
          당된다. 전역 변수(global variable)는 실행코드와 더불어 컴파일 시간에 자
          신의 상대주소와 초기화코드가 결정되므로, EXE코드가 메모리에 올라왔을
          때 이미 EXE의 일부가 되어 있다. 물론 코드(code)와 데이터(data)는 다른
          메모리 블락(16비트 시절에 이것은 세그먼트(segment)로 불렸다)에 코드가
          생성된다. static은 단지 이러한 전역 변수의 범위(scope)를 고정(static)시키
          는 역할을 한다. static으로 선언된 전역변수는 다른 파일에서 extern으로
          참조할 수 없다. 즉, 범위를 파일로 고정시킨다. 특별하게도 지역 변수에
          static변경자가 붙으면, 이 변수는 전역 변수처럼 데이터 블락에 할당된다.
          하지만, 범위가 블락으로 제한된다.

  사용자가 main.exe의 아이콘을 마우스로 더블 클릭(double click)한다.
main.exe가 디스크에서 메모리로 로드(load)된다. 운영 체제 코드는 이미 메모
리에 상주하는 상태이다. 이제 운영체제의 시작 코드(start-up code)가
main.exe의 시작 함수 main()을 호출한다.

  main()이 시작된다. 이 시점에서 전역 변수와 정적 변수, i,j와 k는 이미 메
모리에 로드되어 초기화되어 있다는 것을 상기하자.

  main()의 지역 변수 선언문

int* ip;

가 실행되면, 이 변수는 스택(stack)에 할당된다. 이 변수의 주소를 5000이라
가정하자. 그 다음 문장은 C++의 연산자 new를 사용하여 메모리에서 정수 2
개 크기만큼의 메모리를 할당한다. 이 메모리는 힙에서 할당된다. 4바이트(정
수 2개)의 시작 주소를 2000이라 하자. 다음 문장이 실행된다. 문장

ip[0]=11;

은

*(2000+0)←11;

로 번역되므로, [2000]에 11이 대입된다.

ip[1]=21;

은

*(2000+1×sizeof(int))←21;

로 번역되므로, [2002]에 21이 할당된다.

  자동으로(컴파일러에 의해) 스택에 할당된 변수 ip는 main()이 끝나기 전에
자동으로 해제될 것이다. 하지만, ip가 가리키는 곳의 4바이트는 사용자가 할
당한 코드이므로, 명시적으로 메모리를 해제해 주어야 한다. 그러지 않으면,
해제되지 않은 메모리가 남게 될 것이다.

  전역 변수 i와 j, 그리고 정적 변수 k에 대한 참고는 어떤가? 불법(invalid)
의 참조는 이미 컴파일 시간에 걸러진 상태이므로, i, j와 k에 대한 참조는 각
각 [1000], [1002]와 [1004]를 참고할 것이며, 타당한 참조인지의 검사는 필요
없다.

  main()의 끝 부분의

delete[] ip;

가 실행되면, 힙에 할당된([2000]∼[2003]) 4바이트가 해제된다. main()을 종료
하기 직전 지역 변수 ip가 해제된다. main()이 종료하면, EXE코드가 메모리에
서 삭제되므로, main.exe를 실행하기 전의 상태로 돌아오게 되었다.

  연습문제

1. 이미 만들어져서 .obj형태로 존재하는 C함수들을 C++에서 사용하기 위해
서, C++의 소스코드에서 해 주어야 하는 것은 무엇인가?(Hint: extern "C" 블
락)

2.


12. 배열

12. 4차원의 세계(4-dime
nsional world)

  이 장의 주제는 배열(array)이다. 하지만, 배열을 고려하기 전에 먼저 차원
(dimension)의 개념을 살펴보는 것이 필요하다. 우리가 사는 이 세계는 정말
3차원일까?

  차원(dimension)

  1차원(one dimension)은 기하학적으로 직선(line)을 의미한다. 직선상의 임
의의 위치를 기술하는데 필요한 파라미터의 수는 1개이다. 그래서 이것을 1차
원이라 한다.

             1차원상에서 위치를 기술하는데 필요한 파라미터는 1개뿐이다.

  2차원(two dimension)은 기하학적으로 평면(plane)이다. 평면상의 임의의
위치를 기술하는데 필요한 파라미터의 수는 2개이다. 이것은 평면이 2차원인
이유이다.

             2차원상에서 위치를 기술하는데 필요한 파라미터는 2개이다.

  3차원(three dimension)은 기하학적으로 입체(cube)이다. 입체상의 임의의
한 점을 기술하는데 필요한 파라미터는 3개이다. 그래서 입체는 3차원이다.

             3차원상에서 위치를 기술하는데 필요한 파라미터는 3개이다.

  독자들은 이제 목적 함수(objective function)에 필요한 파라미터의 수에 따
라 차원의 수가 구분된다는 것을 알았을 것이다. 대충 말하면, 어떤 함수의 출
력 값을 결정하는데 3개의 파라미터가 필요하다면, 그 함수는 3차원 함수가
되는 셈이다.

  그러면 우주 상에서 나 자신의 위치를 기술하는데 필요한 파라미터는 몇 개
일까? 지구의 중심이 원점으로 고정된다면, 경도(longitude), 위도(latitude), 고
도(height)로 물체의 위치를 나타낼 수 있으므로, 3차원인 것 같지만, 오늘 내
가 여기 있는 것과, 내일 내가 여기 있는 것은 분명히 다르므로, 마지막 파라
미터인 시간(time)이 고려되어야 한다. 그러므로, 공간은 3차원인 것 같지만,
시공간은 아마도 4차원인 것 같이 보인다.

  그렇다고 이 우주가 4차원일 것이라는 결론을 쉽게 내려서는 안 된다. 어떤
학자는 우주의 법칙을 기술하는데 필요한 차원의 수는 11차원이라고 하였다!

  4차원은 기하학적으로 어떤 모양일까? 우리가 사는 공간은 3차원이기 때문
에, 그 모양을 알 수 없다. 하지만, 3차원 물체를 2차원 평면에 전개도로 나타
낼 수 있듯이, 4차원 물체를 3차원에 전개도로 그리는 것은 가능하다. 이것은
'유전자 알고리즘'에서 자세히 다룬다.

  독자들은 이제 4차원의 세계로 여행할 준비가 되었다. 바로 C++의 4차원
배열(four dimensional array)을 이용하는 것이다.

  같은 형의 변수를 여러 개 선언하는 법

  성적 처리를 하는 프로그램을 코딩한다고 가정하자. 학생의 수가 50명이고
과목의 수가 4과목이라면, 최소한 정수형 변수가 200개 필요할 것이다. 그러면
프로그램은 다음과 같은 긴 변수 선언문을 가질 것이다.

#include <stdio.h>

void main() {

    int i001,i002,i003,i004,i005,i006,i007,i008,i009,i010,

    i011,i012,i013,i014,i015,i016,i017,i018,i019,i020,

    i021,i022,i023,i024,i025,i026,i027,i028,i029,i030,

    i031,i032,i033,i034,i035,i036,i037,i038,i039,i040,

    i041,i042,i043,i044,i045,i046,i047,i048,i049,i050,

    i051,i052,i053,i054,i055,i056,i057,i058,i059,i060,

    i061,i062,i063,i064,i065,i066,i067,i068,i069,i070,

    i071,i072,i073,i074,i075,i076,i077,i078,i079,i080,

    i081,i082,i083,i084,i085,i086,i087,i088,i089,i090,

    i091,i092,i093,i094,i095,i096,i097,i098,i099,i100,

    i101,i102,i103,i104,i105,i106,i107,i108,i109,i110,

    i111,i112,i113,i114,i115,i116,i117,i118,i119,i120,

    i121,i122,i123,i124,i125,i126,i127,i128,i129,i130,

    i131,i132,i133,i134,i135,i136,i137,i138,i139,i140,

    i141,i142,i143,i144,i145,i146,i147,i148,i149,i150,

    i151,i152,i153,i154,i155,i156,i157,i158,i159,i160,

    i161,i162,i163,i164,i165,i166,i167,i168,i169,i170,

    i171,i172,i173,i174,i175,i176,i177,i178,i179,i180,

    i181,i182,i183,i184,i185,i186,i187,i188,i189,i190,

    i191,i192,i193,i194,i195,i196,i197,i198,i199,i200;

}

  실제로 위의 소스가 컴파일된다는 것은 놀라운 일이다. 이제 모두의 성적을
0으로 초기화해야 한다고 하자. 이런! 대입문을 200번 사용해야 할 것이다!

  C를 처음 배우는 독자들은 아마도 위와 같은 난처한 상황에 대해 고민해
본적이 있을 것이다. 여기에 대한 좋은 해결책은 바로 배열을 사용하는 것이
다.

  정수형 변수 i를 선언하기 위해 다음과 같은 선언문을 사용한다.

int i;

  이것이 배열 선언의 출발이다. 만약 정수형 변수 200개를 선언하고 싶다면,
위의 선언에서 변수이름 뒤에 여는 브래킷([)과 닫는 브래킷(])을 쓰고, 그 안
에 변수의 개수를 명시한다.

int i[200];

  여기서 대괄호([, ])안에 쓰는 숫자를 색인(index)이라 한다. 위의 선언에서
색인의 크기는 200인데 이것은 정수형 변수 200개를 선언함을 의미한다. 여기
서 독자들이 주의해야 하는 사항은 색인의 범위는 0부터 199까지라는 것이다.
그러므로 위의 선언은 다음과 같은 변수 200개를 선언한 것이다.

i[0], i[1], i[2], ... ,i[199]

  i[200]은 타당한 배열 요소의 참조가 아니라는 것에 주의하라. 만약 프로그
램 중간에 i[200]이라고 쓴다면, 프로그램은 할당되지 않은 메모리 공간을 접
근하려 할 것이고 결국은 다운(down) 될 것이다■. 또한 i는 배열의 이름이지

             i[200]=0; 이라는 대입문장은 Windows에서 GPF(general protection fault)가
          발생한다. 이 에러는 프로세서가 할당하지 않은 메모리를 접근하려 했을
          때 발생한다. DOS 운영체제에서는 에러가 발생하지 않을 수도 있는데,
          DOS가 엄격하게 메모리 검사를 하지 않기 때문이다. 하지만, 이것은 분명
          히 오류이다.

변수가 아니라는 사실을 주의하라.

  하지만, 배열을 사용하는 진짜 이유는 여러 개의 변수를 간결하게 선언할
수 있어서가 아니다. 배열을 사용하는 이유는, 사용해야 하는 이유는 다음과
같다.

"변수를 이용하여 변수를 참조할 수 있다."

  위의 예에서 i배열의 인덱스의 범위는 0∼199이다. 배열의 인덱스는 다른
변수가 될 수 있다. 그러므로, 배열의 모든 요소를 초기화하기 위해서는 다음
과 같은 for문으로 가능하다.

    ...

    int i[200], j;

    for (j=0;j<200;++j)

        i[j]=0;

    ...

  배열의 i[]의 임의의 요소를 정수형 변수 j를 사용하여 접근하고 있음에 주
목하라. 얼마나 강력한가!

  배열의 인덱스로 사용되는 변수의 개수에 따라 배열의 차원이 결정된다. 위
의 예에서 배열 i[]의 요소를 참조하기 위해서 사용되는 인덱스의 수는 1개이
므로, 1차원 배열이 된다. 즉 배열상의 임의의 위치를 기술하는데 1개의 파라
미터만이 필요한 것이다.

  배열의 차원을 높이기 전에, 더 자세히 1차원 배열에 대해서 알아보자. 1차
원 배열은 과연 메모리에 어떻게 할당되는 것일까? 배열의 이름은 무슨 역할
을 할까? []의 역할은 무엇일까? 배열을 초기화하는 방법은?

  내용 연산자(content-of operator) []

  5개의 요소를 가지는 정수형 배열 a[]를 고려해 보자. 배열 a[]의 요소는 각
각 1,3,5,7과 9로 초기화한다.

#include <stdio.h>

void main() {

    int a[5],i;

    a[0]=1;

    a[1]=3;

    a[2]=5;

    a[3]=7;

    a[4]=9;

    for (i=0;i<5;++i)

        printf("%d,",a[i]);

}

  출력 결과는

1,3,5,7,9,

이다. 위의 예에서 배열의 요소를 초기화하는 것은 어리석어 보인다. 정수를
선언하면서 초기화하듯이, 배열을 선언하면서 초기화할 수 없을까?

  방법은 블락을 이용하는 것이다. 요소의 수가 여러 개이므로, 이 요소 값들
을 블락으로 둘러싼다■.

             실제로 이것은 블락처럼 보이지만 블락이 아니다. 여기서 여는 브레이스
          ({)와 닫는 브레이스(})는 배열의 문법을 구성하는 특별한 심벌이다.

int a[5]={1,3,5,7,9};

  배열의 초기 값이 모두 명시된 경우 배열의 크기를 생략하는 것은 가능하
다. 실제로 위의 선언 및 초기화는 아래와 동일하다.

int a[]={1,3,5,7,9};

  배열의 크기가 생략되었다고 해서, 이것이 크기가 동적으로 변할 수 있는
배열일 것이라고 생각하지 말라. 이것은 단지 첨자 5가 생략되었을 뿐이다. 컴
파일러는 컴파일 시간에 배열의 초기값을 참조하여 배열의 실제 크기를 계산
한다.

  배열의 초기값은 생략이 가능한데, 명시된 값 외에는 초기화가 일어나지 않
는다.

int a[5]={1,3,5};

  위의 예에서 a[0],a[1]과 a[2]는 각각 1,3과 5이다. 하지만, a[3]과 a[4]의 값
은 초기화되지 않는다. 위의 명시적 생략(explicit abbreviation)과 함축적 생
략(implicit abbreviation)은 구분되어야 한다. 함축적 생략은 콤마(,)로 끝이
난다.

int a[5]={1,3,5,};

  위의 생략은 배열의 나머지 요소를 0으로 초기화한다. a[3]과 a[4]는 모두 0
으로 초기화된다. 그러므로 배열의 모든 요소를 초기화하기 위해

    int a[5], i;

    for (i=0;i<5;++i)

        a[i]=0;

이라고 하는 것보다

    int a[5]={0,}■;

             int a[5]={,} 처럼 쓰는 것은 문법 에러이다.

이라고 하는 것이 좋다. 실제로 이 문장은

memset(a,0,sizeof(a));

처럼 동작하므로 빠르다.

  배열의 크기가 개발 시간동안, 빈번하게 변하는 경우라면 어떻게 할까? 바
로 매크로를 이용하여 해결할 수 있다. 독자들은 sizeof연산자를 기억하는가?
위의 예에서 sizeof(a)는 배열 a[]가 차지하는 전체 바이트의 크기를 리턴한다
■. 즉 10(정수가 2바이트를 차지하는 경우)이다. sizeof(a[0])는 얼마일까? 그렇

             sizeof(a)가 배열의 전체 크기를 리턴하는 것은 조금 놀라운 일이다. 상수
          이기는 하지만, a는 int*로 간주되기 때문이다. sizeof(int*)는 분명히 4(포인
          터의 크기)이다. 이것은 sizeof 연산자의 특징이다. sizeof는 변수가 지정되
          면, 변수가 가리키는 대상의 크기를 리턴한다. 그러므로 sizeof를 이용하여
          배열의 크기, 구조체의 크기를 구할 수 있다. sizeof("hello")는 얼마일까? 이
          것 역시 놀랍게도 6이다(EOS를 포함하여). 아마도 char*상수가 지정되면, 0
          을 만날 때까지 문자의 수를 헤아리는 것 같다. 주의 하라. sizeof(char*)는
          분명히 4이다.

다. 2이다. 그러면 배열의 크기 - 첨자의 범위 - 를 계산하는 방법이 떠오를
것이다.

sizeof(a)/sizeof(a[0])

가 그것이다. 이제 소스를 수정해 보자.

#include <stdio.h>

#define SIZE sizeof(a)/sizeof(a[0])

void main() {

    int a[]={1,3,5,7,9},i;

    for (i=0;i<SIZE;++i)

        printf("%d,",a[i]);

}

  이러한 기교 - 크기가 명시되지 않은 배열의 크기를 알아내는 매크로 - 는
후에 구조체(structure)를 다룰 때도 사용할 것인데, 일반적인 기교이다.

  자 이제 배열 a[]가 실제 어떻게 메모리에 할당되는지 살펴보자. 위의 예에
서 main()안에서 사용된 지역 변수를 위해 메모리는 다음과 같이 할당된다.
그림에서 보듯이 배열의 요소가 차례대로 할당된다는 것을 알 수 있다.

             배열의 요소는 낮은 첨자부터 차례대로 할당된다. 배열의 이름 a는 배열
          의 시작 주소를 의미한다.

  배열 a[]가 1000번지부터 할당되었다고 하자. 정수는 2바이트를 차지하므로,
a[0]부터 a[4]까지는 각각 1000번지부터 1008번지까지 할당된다. 메모리의 오
른쪽에 그려진 심벌 테이블에 주목하라. 심벌 테이블은 컴파일러가 코드를 생
성하기 위해, 컴파일 시간에만 유지하는 테이블이다. 이 테이블에 엔트리 a에
[1000]이 들어있다는 사실을 주목하라. 그렇다면, 독자들은 컴파일러가 a[2]를
어떻게 계산하는지 짐작할 수 있을 것이다.

a+2

로 계산한다. 물론 a는 [1000]이므로

[1000]+2

로 계산할 것이다. 여기서 배열은 정수 배열이므로, [1000]+2의 결과는 [1002]
가 아니라, [1000]+sizeof(a[0])*2로 해석하므로, [1004]가 된다. 그러므로 a[2]
는 바르게 a[2]의 메모리 위치인 [1004]를 참조하는 것이다■.

             실제 a가 1000번지라면, a[1]은 1002번지가 아니라, 998번지가 되는데 이
          것은 지역 변수는 스택에 할당되기 때문에 그렇다.

  그렇다면, 2[a]를 컴파일러는 어떤 식으로 해석할까?

2+[1000]

으로 해석할 것이다. 물론 이것은 [1000]+2와 같으므로, a[2]의 내용■을 출력

             독자들은 a[2]가 등호의 왼쪽에 쓰였을 때와 등호의 오른쪽에 쓰였을 때
          의 해석의 차이를 기억하고 있을 것이다. LValue일 때는 주소(address)로
          해석된다. 그러므로 a[2]는 [1000+2]로 해석한다. 여기서 브래킷([])은 안의
          수가 주소임을 의미한다. RValue일 때는 값(value)으로 해석된다. 그러므로
          a[2]는 [1000+2]*로 해석한다. 여기서 위첨자 *는 포인터가 가리키는 내용,
          즉 값을 의미한다.

할 것이다. 아래의 예를 참고하라.

#include <stdio.h>

#define SIZE sizeof(a)/sizeof(a[0])

void main() {

    int a[]={1,3,5,7,9};

    printf("%d,%d,%d\n",a[2],2[a],*(a+2));

    printf("%p,%p\n",a+2,&a[2]);

}

출력 결과는

    5,5,5

    1B87:2026,1B87:2026■

             이 예제는 16비트 컴파일러인 BC++ 4.5에서 컴파일되었다. 주소는 전통적
          인 세그먼트:오프셋 형태로 나타난다.

이다. a[2], 2[a]와 *(a+2)가 같은 값을 가지는 것 - 물론 a[2]라고 쓰는 것이
일반적이다 - 에 주목하라. 또한 a+2의 주소가 &a[2]의 주소와 같음에 주목하
라.

  이제 특별한 형태의 초기화가 가능한 문자 배열(character array)에 대해
서 살펴보자.

  문자열을 관리하기 위해 다음과 같은 배열을 사용한다고 하자. 문자열의 최
대 길이가 5라면 EOS(end of string) 0을 포함하여 길이가 6인 문자열 배열을
사용할 수 있다.

char s[6];

  이제 문자열 배열을 "hello"로 초기화 해 보자.

char s[6]={'h','e','l','l','o',0};

  초기화가 된 경우 배열의 크기를 생략할 수 있으므로, 다음과 같이 적을 수
있다.

char s[]={'h','e','l','l','o',0};

  이 경우 배열의 크기가 명시되진 않았지만, 크기가 6인 문자 배열이 선언된
것이다. 아래의 예제는 문자열 "hello"를 출력한다.

#include <stdio.h>

void main() {

    char a[]={'h','e','l','l','o',0};

    printf("%s\n",a);

}

  문자 배열의 용도가 문자열을 관리하기 위해서라면, 특별한 형태로 배열을
초기화하는 것이 가능하다. 아래의 바뀐 예를 참고하라.

#include <stdio.h>

void main() {

    char a[]="hello";

    printf("%s\n",a);

}

  "hello"는 상수 스트링(constant string) 표현으로 h, e, l, l, o, 0■이 차례대

             문자열의 길이는 5이지만, 6바이트가 할당된다는 사실을 주의하라. 끝에
          스트링의 끝을 나타내는 특수 문자 '\0'이 자동으로 붙는다. 0은 제어 문자
          의 범위인 0∼31사이에 있으므로, Esc 시퀀스를 사용하여 '\0'으로 표현하
          는 것이 일반적이다.

로 메모리에 할당되고, 문자 'h'의 시작 주소를 리턴한다. 그리고, 배열의 이
름이 배열의 첫 번째 요소의 시작 주소를 의미하므로, a[]="hello"는 타당한 것
처럼 보인다. 하지만, 이것은 자연스러운 개념은 아니다. 즉 컴파일러가 문자
배열의 초기화를 위와 같은 형태로 지원하기 때문에 되는 것이지 자연스럽게
되는 것은 아니다. 즉 이것은 특별한 형태의 배열 초기화이다.

             a[]="hello"의 메모리 상태: 배열 a[]는 6바이트의 메모리가 할당되며, 각각
          의 요소가 문자열 "hello" 및 0으로 초기화된다. 'h'가 아니라 실제로 메모
          리에는 ASCII(혹은 ANSI) 코드 값인 104가 2진수 2의 보수 형태로 저장된
          다는 사실에 주의하라. 또한 a는 배열의 시작 주소로서 컴파일 시간에 결
          정된 상수 포인터임에 주의하라.

  위의 메모리 맵을 참고하여 다음 프로그램의 출력 결과를 예측해 보라.

#include <stdio.h>

void main() {

    char a[]="hello";

    printf("%c,%d,%d\n",*(a+1),*(a+1),*(a+5));

}

출력 결과는

e,101,0

이다(%c는 ASCII 문자를 %d는 십진수를 출력한다).

  자 이제 배열을 다룰 때 주의해야 하는 사항에 대하여 살펴보자. 이 부분은
매우 중요하며, 꽤 능숙한 프로그래머조차도 흔히 저지르는 실수이다. 결론부
터 말하면 다음과 같다.

"할당되지 않은 메모리에 문자열을 대입하지 마라."

  그렇다면 아래의 예에서 에러를 찾아 보라. 에러가 전혀 없는 것 같지만, 중
대한 실수를 범하고 있다. 물론 문법 에러(syntax error)는 존재하지 않는다,
다만 논리 에러(semantic error)가 존재하며, 이것은 실행시 에러(run-time
error)를 발생할 수 있다.

#include <stdio.h>

void main() {

    char* s;

    s="hello";//이 문장은 타당한가?

    printf("%s\n",s);

}

  출력 결과는

hello

이다. 실제로 이 프로그램이 제대로 실행된다는 것은 놀라운 일이다. 어디가
잘못인가? 다음의 메모리 상태를 참고하라.

             할당된 메모리는 s를 위한 4바이트뿐이지, 문자열 "hello"를 위해서는 메
          모리가 할당되지 않았다. "hello"라는 표현은 메모리의 남는 곳(?)에 문자열
          hello와 0을 차례대로 집어넣고 'h'의 시작 주소인 2000을 리턴한다. s는
          2000이라는 값을 가지게 되지만, 2000번지부터 2005번지까지가 메모리 할
          당된 것은 아니다.

  위의 그림에서 보듯이 s는 할당되지 않은 문자열의 시작을 가리키고 있다.
그렇다면 "hello"는 메모리의 어디에 할당되는 것인가? 이 질문에 대한 명확한
답을 필자는 알지 못한다. C++ 컴파일러는 프로그램 코드를 생성할 때, CRT
힙(C++ run-time heap)을 할당하는 초기화 코드를 생성한다. 아마도 "hello"등
의 문자열은 CRT 힙의 어느 특정한 부분에 잠시 할당되는 것 같다. 프로그램
코드에 의해 자동으로 유지되므로, 명시적인 할당과 해제 없이도 동작하는 것
같다. 실제로 이 코드는 스트링의 복사 생성자(copy constructor)를 다룰 때
한 번 더 보게 되는데 그 때 더 자세히 살펴보도록 하자. 다시 한번 결론을
말하면, "프로그래머에 의해 할당되지 않은 메모리 영역에 문자열을 대입하여
연산을 시도하지 말라." 즉 위의 코드는 에러 경향이 있다(error prone).

  그래서 코드를 수정하기로 했다. 이번에는 배열을 이용하여 명시적인 메모
리 할당 후에 문자열 대입을 시도하였다. 윽! 이번에는 컴파일 시간 에러가
발생한다. 진행하기 전에 그림을 보고 에러를 찾아 보라. 어느 문장이 왜 에러
인가?

#include <stdio.h>

void main() {

    char s[6];

    s="hello";

    printf("%s\n",s);

}

             메모리 할당: s는 메모리의 어느 곳에도 할당되지 않았다. 다만, s[]의 요
          소(element)들이 메모리에 할당되었을 뿐이다. 즉 s는 상수 주소 - 배열의
          시작을 가리키는 - 로서 존재할 뿐이다.

  s가 메모리에 할당된 것이 아니라, s[0]부터 s[5]까지가 메모리 할당된 사실
에 주의하라. 물론 s도 배열의 인덱스와 함께 요소를 참조하기 위해 실행 시
간에 유지되고 있다. 하지만, s의 값이 바뀌어서는 안 된다. s의 값이 바뀌는
것은 한 번 할당된 배열의 시작 위치가 바뀌는 것이므로 불가능하다. 즉, s는
상수 포인터(constant pointer)로 동작한다. 독자들은 아래의 문장이 에러인 것
을 알고 있다.

2=i;

  위 문장은 i의 값을 상수 2에 대입하려고 시도한다. 이것은 명백한 에러이
다. 다음의 문장도 마찬가지이다.

s="hello";

  위 문장은 'h'의 시작 주소값을 상수 s에 대입하려고 시도한다. 그러므로,
에러인 것이다. 그래서 회심의 미소를 머금고 프로그램을 아래처럼 수정하였
다. 이런! 여기에도 또한 에러가 존재한다.

#include <stdio.h>

void main() {

    char* s;

    s=new char[6];

    s="hello";

    printf("%s\n",s);

    delete[] s;

}

  아래의 메모리 할당 상태를 참고하라.

             s는 할당된 메모리의 시작 주소를 가리킨다. 만약 new가 1500번지부터 6
          바이트를 할당했다면, s는 [1500]이 된다. 그러므로, s가 가리키는 1500번지
          에서부터 문자열 "hello"의 문자들을 차례대로 집어넣어야 한다. 하지만
          s="hello"란 문장에 의해 s의 값은 2000으로 변경되어 할당되지 않은 메모
          리를 가리킬 뿐 만 아니라, 1500을 가리키는 포인터가 사라져서 s는 댕글
          링(dangling) 포인터가 된다. 즉, 1500번지부터 6바이트의 메모리가 샌다
          (leak).

  s="hello"란 문장은 s의 값을 변경시킬 뿐이다. 그러므로, 2000번지부터의 내
용을 차례대로(0을 만날 때까지) 1500번지로 복사해주는 코드를 작성해야 한
다. 이것은 표준 함수로 이미 존재한다. 함수의 이름은 strcpy()이다. 이것은
string.h에 존재한다. 이제 최종적으로 완성된 완전한 소스를 작성할 수 있다.

#include <stdio.h>

#include <string.h>

void main() {

    char* s;

    s=new char[6];

    //s="hello";

    strcpy(s,"hello");

    printf("%s\n",s);

    delete[] s;

}

  이제 다차원(multi-dimensional) 배열을 살펴보자.

  이차원 배열(two-dimensional array)

  이차원 배열은 배열의 요소를 참조하기 위해 2개의 인덱스를 사용한다. 이
것은 2차원 평면, 즉 2차원 테이블을 나타낸다. 위의 예에서 학생의 수가 50
명, 과목이 4과목인 경우는 1차원 배열을 사용하는 것보다 2차원 배열을 사용
하는 것이 더 자연스럽다.

             50명의 성적 처리를 위한 (2차원) 테이블: 특정한 성적을 참조하기 위해서
          는 (번호, 과목)이 필요하다. 2차원 배열 score[50][4]를 선언해서 번호는 줄
          (row)로 과목은 열(column)로 참조한다.

  위의 그림처럼 50행(row), 4열(column)의 테이블을 만들기 위해서 배열을
다음과 같이 선언하다.

int score[50][4];

  첫 번째 인덱스가 행의 수를, 두 번째 인덱스가 열의 수를 지정하는 것에
주의하라. 문제를 간단히 하기 위해 2×4의 score[2][4] 배열을 생각해 보자.
테이블은 다음과 같이 설정되어 있다.

             score[2][4] 배열: 첫 번째 인덱스 2가 행(row)의 수를, 두 번째 인덱스 4가
          열(column)의 수를 나타낸다.

  이것은 배열로 다음과 같이 선언할 수 있다.

int score[2][4];

  1차원 배열을 초기화하듯이, 2차원 배열도 초기화할 수 있는데, 각각의 행을
블락으로 묶어 나타낸다.

int score[2][4]={{1,3,5,7},{10,30,50,70}};

  메모리는 1차원 주소를 가지기 때문에, 2차원 배열이 메모리에 실제 저장될
때는 1차원으로 저장된다는 것을 주의하라. 그렇다면, 행이 먼저 저장되는가,
열이 먼저 저장되는가?

  배열의 행을 먼저 저장하는 방식을 행 중요 순서(row major order)라 하
고, 열을 먼저 저장하는 방식을 열 중요 순서(column major order)라 한다.
FORTRAN을 제외한 모든 언어는 행 중요 순서를 채택한다.

             행 중요 순서: C++의 배열은 행 중요 순서를 채택했다. 그림에서 화살표
          그려진 방향으로 메모리에 저장된다. 한 행의 저장이 끝나면 다음 행을 메
          모리에 저장한다.

  score[] 배열은 실제로 메모리에 아래 그림의 (a)처럼 저장된다.

             (a) 행 우선. (b) 열 우선: score[2][4]의 내용은 C/C++에서 (a)처럼 저장된다.
          FORTRAN언어는 (b)방식으로 저장된다. 무슨 방식이든 간에 배열의 이름
          score는 배열의 첫 번째 요소(element)의 시작 주소를 가리킨다. 이 그림에
          서는 배열은 [1000]부터 할당되었다.

  1차원 배열에서 배열의 인덱스를 생략하면, 배열의 시작 주소를 의미(상수
포인터)한다고 했다. 2차원 배열에서도 배열의 인덱스를 생략할 수 있는데, 생
략된 인덱스를 0으로 한 곳의 시작 주소를 의미한다. 예를 들어, score[0]은
score[0][0]의 시작 주소이다. score[1]은 score[1][0]의 시작 주소이다.

  2개의 인덱스를 모두 생략한 score는 어떤가? 물론 score[0][0]의 시작 주소
이다. 하지만, 첨자를 2개 생략했으므로, 포인터의 포인터(pointer to pointer)
로 해석을 한다. 그러므로 *score는 score[0]을 의미하며, **score가
score[0][0]을 의미한다■.

             score[0]이든 score든 상수 포인터임을 주의해야 한다. 이 값은 컴파일 시
          간에 결정되어 변경되지 않는다. 비록 score가 메모리 할당은 이루어지지
          않지만, 컴파일 시간에 포인터의 포인터 형으로 알려져 있으므로, 간접 지
          정 연산자 *를 2번 사용해야 배열의 요소를 참조한다. 이것은 물론 컴파일
          시간에 결정된다.

             score의 해석: score[0]은 pointer to score[0][0] 즉, &score[0][0]을 의미한다.
          score는 pointer to score[0] 즉, &score[0]으로 해석을 한다. 그러므로, 배열
          의 요소 정수를 참조하기 위해서는 2번 재참조(indirection)해야 한다.

  아래 프로그램의 출력 결과를 예측해 보기 바란다.

#include <stdio.h>

void main() {

    int score[2][4]={{1,3,5,7},{10,30,50,70}};

    printf("%d,%d\n",score[0][1],score[1][2]);

    printf("%d,%d\n",*(score[0]+1),*(score[1]+2));

    printf("%d,%d\n",*(*score+1),*(*score+6));

}

  결과는

    3,50

    3,50

    3,50

이다.

   score[1]+2가 [1008]+2이므로 [1010]이라고 해석해서는 안 된다. 포인터
의 덧셈과 뺄셈은 포인터의 대상의 크기만큼이 고려된다. score[1]이 정수 포
인터이므로 score[1]+2는 score[1]에서 2번째 정수를 의미한다. 그러므로
score[1]+2는 [1012]이다. 일반적으로 포인터 변수 pointer에 정수 n을 더하거나
뺀

pointer±n

의 의미는

pointer±sizeof(*pointer)×n

이다.

  2차원 배열의 요소(element)들이 행 순서로 차례대로 저장되는 것을 알았을
것이다. 그러므로 아래와 같은 초기화는

int score[2][4]={{1,3,5,7},{10,30,50,70}};

  다음과 같이 쓸 수 있다.

int score[2][4]={1,3,5,7,10,30,50,70};

   다음 두 개의 초기화는 어떻게 다른가?

    int score[2][4]={{1,3,5},{10,30,50,70}};

    int score[2][4]={{1,3,5,},{10,30,50,70}};

   첫 번째 초기화에서 score[0][3]은 초기화되지 않는다. 하지만 두 번째
초기화는 score[0][3]을 0으로 초기화한다.

  2차원 문자 배열은 1차원처럼 간단하게 쓸 수 있다.

#include <stdio.h>

void main() {

    char str[4][10]={{'g','o','l','d',0},

                     {'s','i','l','v','e','r',0},

                     {'c','o','p','p','e','r',0},

                     {'n','e','c','k',0}};

    int i;

    for (i=0;i<4;++i) {

        printf("%s\n",str[i]);

    }//for

}

  위의 예에서 문자 배열 str[]은

    char str[4][10]={{"gold"},

                     {"silver"},

                     {"copper"},

                     {"neck"}};

와 동일하다. 물론

    char str[4][10]={"gold",

                     "silver",

                     "copper",

                     "neck"};

와도 동일하며, 이것이 가장 일반적이다.

  배열이 초기화되어 배열의 크기가 알려진 경우는 첨자를 생략할 수 있다.
위의 예에서 문자열이 4개이므로, 행의 크기가 4인 것을 알 수 있으므로, 첫
번째 인덱스 4는 생략할 수 있다.

    char str[][10]={"gold",

                    "silver",

                    "copper",

                    "neck"};

  하지만, 마지막 차원의 인덱스는 생략할 수 없다. 위의 예에서 str[][10]의
10을 생략하여 str[][]처럼 쓸 수는 없으며, str[4][]처럼 쓸 수도 없다. 컴파일
러가 컴파일 시간에 배열의 크기를 결정하기 위해서 반드시 마지막 첨자는 명
시해 주어야 한다.

3차원 이상의 배열

int threeDim[3][2][4]={{{1,2,3,4},{5,6,7,8}},

                       {{9,10,11,12},{13,14,15,16}},

                       {{17,18,19,20},{21,22,23,24}}};

  3×2×4■의 3차원 배열은 위처럼 선언한다. 이것은 기하학적으로 다음 그림

             이것은 수학 좌표 공간상에서 Z×Y×X축의 단위를 의미한다. 즉 3은 Z축,
          2는 Y축, 4는 X축으로의 크기이다. 만약 독자들이 X축의 크기가 n, Y축의
          크기가 m인 2차원 판(board) 배열을 선언하기 위해서는 board[m][n]처럼 선
          언해야 한다. 배열의 첫 번째 인덱스는 행, 즉 Y축을 의미한다.

과 같은 의미를 가지며, 메모리에 그림과 같이 저장된다.

             3차원 배열: 3차원 배열은 기하학적으로 입체를 나타낸다. 행 우선 순서
          로 메모리에 저장되므로, Z축 상의 첫 번째 평면(1,2,...)이 메모리에 저장된
          다. 이 평면이 저장될 때는 물론 행이 먼저 저장된다. 그 다음에 두 번째
          평면(9,10,...)이 저장된다. 마지막으로 세 번째 평면(17,18,...)이 저장된다.

             3차원 배열의 이름의 해석: threeDim[0][1]은 &threeDim[0][1][0]를 의미한다.
          threeDim[0]은 &threeDim[0][0], 즉 threeDim[0][0][0]에 대한 포인터의 포인터
          를 의미한다. threeDim은 &threeDim[0], 즉 threeDim[0][0][0]에 대한 포인터의
          포인터의 포인터(pointer to pointer to pointer)를 의미한다. 그러므로
          threeDim을 포인터 변수 ip에 대입하기 위해서는 ip는 int*** ip; 처럼 선언
          되어야 한다.

  4차원의 기하학적인 그림을 2차원에 그릴 수 없다(3차원에는 그릴 수 있는
데 이것은 '유전자 알고리즘'에서 설명한다). 4차원 이상의 배열이 문법적인
에러는 없음에도 불구하고, 충고(advice)는 다음과 같다.

"프로그램에서 4차원 이상의 배열을 사용하지 말라. 만약 독자들의
프로그램이 4차원 배열을 필요로 한다면, 프로그램의 설계가 틀린
것이다."

  배열의 전달

  배열을 함수의 파라미터로 어떻게 전달할까? 배열의 요소를 모두 전달하는
것은 스택의 오버헤드 때문에 좋은 방법이 아니다(만약 정수 배열의 크기가
30000이라면 이미 할당된 메모리 60000바이트를 제외하고 스택에 다시 60000
바이트가 할당되어야 한다. 또한 이 60000바이트가 복사되어야 한다. 속도 면
에서 효율 면에서 감당해야할 댓가(price)가 너무 크다).

  배열은 항상 배열의 시작 주소가 전달된다. 주소가 전달되었을 때의 부효과
(side effect)를 기억하는가? 함수 안에서 배열의 요소를 변경하면, 함수를 호
출한 쪽에 영향을 미치므로, 호출한 쪽의 배열의 요소가 바뀌게 된다. 아래의
예를 참고하라.

#include <stdio.h>

void f(int score[][4]) {

    score[1][1]++;

}//score

void main() {

    int score[][4]={{1,3,5,7},{10,30,50,70}};

    printf("%d\n",score[1][1]);

    f(score);

    printf("%d\n",score[1][1]);

}

  출력 결과는

30

31

이다. 함수 f()가 함수를 받는 방법에 주목하라.

void f(int score[][4]);

비록 2차원 배열처럼 파라미터에 선언되었음에도 불구하고, 이것은 score가 2
차원 배열의 시작 주소임을 선언한 것이다. 두 번째 인덱스는 생략해서는 안
된다. 2번째 인덱스가 생략되면 2개의 첨자를 이용해서 배열 요소의 상대위치
를 구할 수 없게 된다.

   score가 2차원 배열의 시작 주소를 의미한다고 했으므로, 함수 f()에
서 void f(int** score); 처럼 배열의 시작 주소를 받는 것은 가능한가?

   불가능하다. int score[][]가 될 수 없는 것과 같은 이유이다. int** score
로 명시되면 2번째 차원의 크기를 알 수 없으므로, 배열의 요소를 2개의 첨자
를 이용해서 꺼내는 것이 불가능해진다. 실제로 컴파일러는 int a[1][3]; 으로
선언된 배열의 주소 a와, int b[1][2]; 로 선언된 배열의 주소 b를 구분하여 컴
파일한다.

  하지만 1차원인 경우는 1개의 첨자로 배열의 요소를 결정하므로, 정수 배열
의 시작 주소를 int* score 형태로 받는 것이 가능하다. 실제로 함수의 파라미
터 선언에서 int* score는 int score[]와 동일하다. 아래의 예를 참고하라.

#include <stdio.h>

void f(int score[]) {

    ++(*(score+2));

}//score

void main() {

    int score[4]={1,3,5,7};

    printf("%d\n",score[2]);

    f(score);

    printf("%d\n",score[2]);

}

  위의 예에서 함수 f()의 정의 부분을

    ...

    void f(int* score) {

        ++(*(score+2));

    }//score

    ...

처럼 바꾸어도 의미는 같다. 출력 결과는 물론

5

6

이다.

  void f(int score[4]) 처럼 첨자의 크기를 생략하지 않아도 된다. 컴파일러는
단순히 4를 무시한다. 그러므로, void f(int score[400])처럼 첨자의 크기가 틀
려도 에러는 발생하지 않는다.

  포인터 배열

char c;

  위의 문장은 1바이트 정수형(문자형) 변수 c를 선언한 것이다.

char c[10];

위의 문장은 크기가 10인 문자형 배열을 선언한 것이다.

char* c;

위의 문장은 문자형 포인터 변수를 선언한 것이며, 메모리에는 4바이트가 할
당된다. 포인터 선언과 배열 선언을 결합하면 포인터 배열을 선언할 수 있다.

  크기가 4인 문자형 포인터 배열은 다음과 같이 선언한다.

char* str[4];

  이것은 str[0], str[1], str[2]와 str[3]이 char* 형인 배열을 선언한 것이다.
그러므로 할당되는 메모리는 4×4=16바이트이다. 아래의 예는 이 배열에 문자
열을 대입(문자열의 선두 문자의 주소를 대입)한 다음, 이를 출력하고 있다.

#include <stdio.h>

void main() {

    char* str[4]={"gold",

                  "silver",

                  "copper",

                  "neck"};

    int i;

    for (i=0;i<4;++i)

        printf("%s\n",str[i]);

}

  출력 결과는

    gold

    copper

    silver

    neck

이다.

  str을 파라미터로 받는 함수 f()는

void f(char* str[4]);

혹은

void f(char* str[]);

혹은

void f(char** str);

로 선언할 수 있다. 아래의 예를 참고하라.

#include <stdio.h>

void f(char** str) {

    int i;

    for (i=0;i<4;++i)

        printf("%s\n",str[i]);

}

void main() {

    char* str[4]={"gold",

                  "silver",

                  "copper",

                  "neck"};

    f(str);

}

  독자들은 위의 main()안에서 선언된

    char* str[4]={"gold",

                  "silver",

                  "copper",

                  "neck"};

와, 2차원 배열을 사용한 이전의

    char str[4][7]={"gold",

                    "silver",

                    "copper",

                    "neck"};

를 구분해야 한다. 실제로 문자열은 2번째처럼 사용되어야 한다. 1번째 사용
(포인터 배열)은 명시적으로(explicitly) 문자열을 위한 메모리 공간을 할당하
지 않는다. 또한 2번째 경우는 파라미터를 받기 위해

void f(char** str);

처럼 지정할 수 없다. 아래 그림의 (a)와 (b)는 각각 2가지 경우의 메모리 상
태를 보여준다. 다시 강조한다.

"명시적으로 할당되지 않은, 메모리 공간을 사용하지 말라."

             (a) char* str[4]의 메모리 할당. (b) char str[4][7]의 메모리 할당: (a)의 경우
          할당된 메모리 공간은 1000번지부터 1015번지까지의 16바이트이다. 하지만
          (b)의 경우 할당된 메모리 공간은 1000번지부터 1027번지까지 28바이트이
          다. (a)의 경우 문자열을 위한 메모리 공간은 할당되지 않았다. 다만, 문자
          열의 포인터만 할당되었을 뿐이다. 하지만 (b)의 경우 문자열을 위한 메모
          리 공간이 명시적으로 할당되었다.

  울타리 막대기 문제: 가장 자리 문제

  배열과 연관된 주의 사항은 '울타리 막대기 문제'로 알려져 있다. 4개의 울
타리를 만들기 위해서 필요한 막대기는 5개라는 것을 주의하라.

             울타리 막대기 문제: 울타리보다 1개 많은 막대기가 필요하다.

  예를 들면 크기가 5인 배열의 인덱스는 1∼5가 아니라, 0∼4이다.

  길이가 5인 문자열을 관리하기 위해서는 크기가 6인 문자열 배열이 선언되
어야 한다.

  배열의 요소를 계산하기 위해, 앞뒤로 인접한 배열의 셀(cell)을 검사할 때,
처음에서는 앞을 검사하면 안되며, 마지막에서는 뒤를 검사하면 안된다. 이 주
제는 'board가 필요하면, border를 만들라'에서 자세히 설명한다.

  배열의 모든 요소를 1로 초기화하는 for문을 수행했을 때, for문 수행이 끝
났을 때의 인덱스는 유효하지 않다.

    int a[5],i;

    for (i=0;i<5;++i)

        a[i]=1;

에서 for문 실행후의 i 값은 5이며, 5는 a[]의 타당한 인덱스가 아니다. 이러한
가장 자리 문제는 주의 깊게 다루어야한다.

  진보된 주제

  객체(object)의 배열을 만들 수 있다. 이것은 후에 클래스(class)를 설명한
뒤, "생성자, 소멸자"에서 다룬다. 또한 new와 delete에서 사용되는 []는
"new와 delete"에서 다룬다(new와 delete연산자에서 사용되는 심벌 []는 내
용 연산자가 아니다. new와 delete의 문법을 이루는 특수 심벌일 뿐이다).

  실습문제

1. (Zeller의 공식) 아래의 프로그램에서 DayOfWeek()는 년, 월, 일을 파라미
터로 받아 주일, 월요일, ... ,토요일에 대해 0,1, ... ,6을 리턴한다.
DayOfWeek()를 자세히 설명하라. 또한 main()에서 2차원 배열의 역할에 대해
서도 자세히 설명하라(힌트: 윤년은 4의 배수면서, 100의 배수가 아니면서,
400의 배수이면 성립함. 또한 0년 1월 1일은 당연히 1주일의 시작인 일요일
임).

#include <stdio.h>

int DayOfWeek(int nYear,int nMonth,int nDay) {

    if (nMonth<3) {

        nYear-=1;

        nMonth+=12;

    }

    return (nYear+nYear/4-nYear/100+nYear/400+(13*nMonth+8)/5+nDay)%7;

}

void main() {

    char s[7][20]={"SUN","MON","TUE","WED","THU","FRI","SAT"};

    printf("%s\n",s[ DayOfWeek(0,1,1) ]);

    printf("%s\n",s[ DayOfWeek(1999,3,9) ]);

    printf("%s\n",s[ DayOfWeek(1999,3,10) ]);

}

2.


2. 참조표(Look-up Table)

2. 참조표(Look-up Tabl
e)

  다음과 같은 얘기(fairy story)가 있다.

  대학 4학년생이 졸업할 때가 되었다. 최대의 관건은 (1) 직장은 구했느냐?
(2) 결혼할 여자는 있느냐? 이다. 유무에 따라 4가지로 구분해 보면,

  (1) 직장도 있고(=1), 애인도 있다(=1): 금메달

  (2) 직장은 있고(=1), 애인은 없다(=0): 은메달

  (3) 직장은 없지만(=0), 애인은 있다(=1): 동메달

  (4) 직장도 없고(=0), 애인도 없다(=0): 목메달

로 구분해 볼 수 있다. 아무래도 남자에게는 애인보다 직장이 우선인 모양이
다. 만약 여자 졸업생이라면 애인이 우선 순위일 수 있다. 그렇다고 경우 4에
해당하는 독자가 '목메달' 하기를 원하는 것은 아니다.

             목메달하기 직전의 남자 그림

  위와 같은 얘기를 프로그래밍 한다고 생각해 보자. 아마도 직장의 유무와
애인의 유무를 관리하기 위해 독자들은 2개의 변수를 선언할 것이다. 완성된
클래스는 다음과 같다.

#include <stdio.h>

class CAlumni {

    int bWork,bLover;

  public:

    CAlumni(int bWork,int bLover) {

        CAlumni::bWork=bWork;

        CAlumni::bLover=bLover;

    }//CAlumni

    void State();

};//class CAlumni

void CAlumni::State()

{

    int i;

    if (bWork==1 && bLover==1) i=0;

    else if (bWork==1 && bLover==0) i=1;

    else if (bWork==0 && bLover==1) i=2;

    else if (bWork==0 && bLover==0) i=3;

    if (i==0) printf("금메달\n");

    else if (i==1) printf("은메달\n");

    else if (i==2) printf("동메달\n");

    else if (i==3) printf("목메달\n");

}//CAlumni::State

void main()

{

    CAlumni kim(1,0);

    kim.State();

}//main

  아마도 여러분은 State() 메서드의 첫 번째 if 블락이 맘에 들지 않아 다음
과 같이 바꿀 수도 있을 것이다.

i=(bWork<<1) | bLover;

  두 번째 if 블락은 i를 인덱스로 하고, 출력할 문자열을 내용으로 하는 테이
블(Table)을 이용하면 간단하게 구현된다. 배열이 이러한 용도로 사용되었을
때, 참조표라고 한다(형식적인 정의에 익숙한 어떤 독자들은 이러한 정의가
마음에 들지 않을 지 모른다. 하지만 기억해 둘 것은 이 책은 비 전공자들의
프로그래밍 능력을 위해 쓰여진 책이란 것이다).

  바뀌어진 State() 메서드는 다음과 같다.

char sTable[][7]={"금메달","은메달","동메달","목메달"};//전역변수

void CAlumni::State()

{

    int i;

    i=(bWork<<1) | bLover;

    printf("%s\n",sTable[i]);

}//CAlumni::State

  일반적으로 미리 알려진 내용들을 이용할 때 이러한 참조표를 사용한다. 참
조표는 프로그램을 간단하고 보기 쉽게 한다.

  점수에 따라 등급을 출력하는 경우

  일선 학교의 교사들은 성적 처리 프로그램을 만드는 데, 등급을 출력하기를
원할지도 모른다.

점수대

등급

90~100

수

80~89

우

70~79

미

60~69

양

0~59

가

             등급표

  점수대와 등급 문자열이 <표 ?>와 같다고 할 때, 이를 참조표로 구현하면
아래의 소스와 같다. 배열의 인덱스를 구하기 위해 % 연산자의 기교
(Technique)을 사용했다는데 주의하기 바란다. 이러한 기교는 종종 그리고 유
용하게 사용된다.

    char *sTable[]={"가","가","가","가","가",

                     "가","양","미","우","수","수"};

    int i;

    scanf("%d",&i);

    printf("%s\n",sTable[i%10]);

기교(technique)

  잠시 배열의 인덱스 등을 계산하는데 사용하는 몇 가지 기교를 살펴보자.

  (1) 0과 1을 토글(Toggle)하는 경우: 프로그램에서 변수의 값을 0↔1을 토
글하기 위해서는 다음의 문장을 사용한다.

    int i=0;

    i^=1;//비트 배타합(eXclusive Or) 연산의 특징을 이용한다.

  이와 같은 기교는 임의의 두 수를 토글하는데 사용할 수 있다. 예를 들어 1
과 3을 토글하려면,

0001 ^ 0011 → 0010

이므로, 두 수의 배타합의 결과인 2와 배타합 연산을 하면된다.

    int i=1;

    i^=2;//1과 3을 토글한다.

  (2) 1과 -1을 토글하는 경우

    int i=1;

    i=-i;

  (3) 0,1,2,3을 토글하는 경우: 2개 보다 많은 숫자를 토글해야 하는 경우는
나머지 연산자 %를 사용해야 한다.

    int i=0;

    i=(i+1)%4;

  만약, 5,6,7,8을 토글하고 싶다면,

    int j=0, i;

    j=(j+1)%4;

    i=j+5;

처럼 사용한다. 일반적으로 m에서 시작하고 연속된 n개의 숫자를 토글하려면,

    int j=0, i;

    j=(j+1)%n;

    i=j+m;

을 사용하면 된다. 그러면 임의의 숫자열을 토글하는 방법은 없을까?

임의의 숫자열을 토글하려면

  토글을 원하는 숫자열이

100→34→200→79

처럼 규칙성이 없는 임의의 숫자들이라고 하자. 이러한 숫자열을 토글하는 방
법은 참조표를 이용하는 것이다. 이러한 값들을 참조표에 미리 넣어두고 배열
의 인덱스를 토글하면 간단히 구현할 수 있다.

    int toggle[]={100,34,200,79};

    int j=0, i;

    i=(i+1)%4;

    j=toggle[i];//얼마나 간단한가!

  임의의 사상(Mapping)을 구현

  이번에는 <그림 ?>와 같은 함수를 구현하는 문제를 고려해 보자. 사용자가
입력하는 값에 따라, 대응하는 치역(Range)의 값이 출력되어야 한다.

              어떤 사상

  이러한 것은 정의 구역(Domain)의 원소도 치역의 원소도 임의의 값에 해당
하므로, 2차원 참조표로 구현해야 한다.

  여러분이 직면하는 어떤 문제에 적절한 참조표가 생각날 수 있도록 하기 위
해서는 많은 연습과 경험이 필요하다■. 소스는 아래에 리스트하였다.

             필자의 경험으로 비추어 볼 때, 365시간 이상의 투자가 필요하다. 여러분
          이 C++ 프로그래밍에, 하루 1시간씩 1년여의 투자를 한다면, 독자들은 자
          신도 모르는 사이에 전문가가 되어 있을 것이다.

                 C++은 10대의  성(sex)과 같다. "1-모두의 마음속에 항상 존재한다.", "2-
          모두가 항상 이에 대해 이야기한다.", "3-다른 사람들은 모두 이에 대한 경
          험이 있다고 생각하지만, 실제로 경험한 사람은 거의 없다.", "4-극히 일부
          의 경험자들조차 제대로 시행해보지 못했으며, 또한 안전하게 시행하지도
          못했다.", "5-그러나 모두들 다음 번에는 잘 할 수 있으리라는 환상을 가지
          고 있다.", "6-모두가 자신들의 성공담을 자랑하고 다닌다. 실제로는 경험
          해 본적도 없으면서."(이스라엘의 어느  화장실 낙서에서)

#include <stdio.h>

int mapping[5][2]={{34,876},{125,56},{18,12},{345,11},{22,23}};

void main() {

    int i,n;

    scanf("%d",&n);

    for (i=0;i<5;++i)

        if (n==mapping[i][0]) {

            printf("%d\n",mapping[i][1]);

            break;

        }//if

}//main

  실습문제

1. 512와 1024를 토글하는 문장을 작성하시오.(힌트: 512 XOR 1024)

2. 변위(Offset)가 포함된 좌표의 검사에는 참조표를 사용하여야 한다. 아래
그림은 컴퓨터 바둑(Computer Go)에서 돌(Stone)들간의 관계를 고려하기
위해, 사용할 참조표이다. 예를 들면, 중앙의 검은 돌 입장에서, 8과는 한칸,
13과는 날일자(日)의 관계가 있다. 이를 참조표로 구현하라. 참조표의 인덱스
가 그림처럼 딱지되었다는 것에 주목하라. 인덱스가 그림처럼 딱지 되지 않으
면, 어떤 불편함 점이 발생하는지 고려해 보라■.

             실제로 눈목자(目) 등도 고려하여야 하므로, 참조표는 이보다 훨씬 크다.
          또한 변위가 (x,y)의 위치에서 2칸까지 변하므로, 판(Board)의 가장자리도 2
          보다는 크게 정해야지 구현이 쉽다. 판이 고려될 때 가장자리에 관한 문제
          는 뒤에서 상세히 다룬다.

              컴퓨터 바둑에서 돌들간의 거리

3. 한글 완성형 코드를 조합형 코드로 바꾸는 클래스를 설계하시오■.

             완성형과 조합형의 코드 값은 프린터 매뉴얼이나 한글 구현과 관련된 서
          적을 참고해야 알 수 있다.

4. 연결된 요소(Connected Component)를 발견하는 문제: 2차원 평면에서
셀(Cell)들간의 관계는 인접한 이웃(Adjacent Neighbor)으로써 표현하는데, 4-
연결 이웃(4-Connected Neighbor: Von Neumann Neighbor)과 8-연결
이웃(8-Connected Neighbor: Moore Neighbor)■이 일반적이다. 한 셀에

             Jörg R. Weimar, "Simulation with Cellular Automata: Lecture Notes",
          Braunschweig, June 1996, p.17.

서의 인접한 이웃은 참조표(Look-up Table)를 사용해서 구현할 수 있지만,
연결된 요소를 찾는 문제는 참조표만으로는 구현이 어려우며, 스택을 사용하
면 구현이 가능하다.

             이웃

  <그림 ?>에서 셀에 딱지된 번호는 참조표의 인덱스를 의미하는데, 주의해
서 보기 바란다. 위와 같이 딱지를 매기면, 한 개의 참조표만으로 2가지 이웃
을 쉽게 참조할 수 있다.

  참조표를 아래와 같이 전역변수로 선언한 다음,

int offset[8][2]={{1,0},{0,-1},{-1,0},{0,1},{1,-1},{-1,-1},{-1,1},{1,1}};

  노이먼 이웃으로 이용할려면,

for (int i=0; i<4; ++i)

    if ((x+offset[i][0], y+offset[i][1]) is my condition) ...

  무어 이웃으로 이용할려면,

for (int i=0; i<8; ++i)

    if ((x+offset[i][0], y+offset[i][1]) is my condition) ...

처럼 i값만을 변경시켜 구현하면 된다.

  (1) 지뢰찾기(Mine Sweeper)■ 게임에서 연속한 빈칸을 발견하는 클래스

             지뢰 찾기는 Win95TM에 들어 있는 게임 프로그램이다.

를 설계하시오.

  왼쪽 그림에서 처럼, 사용자가 지뢰가 없는 빈 부분을
마우스로 클릭했을 때, 숫자가 있는 셀을 포함하여 빈
칸 모두를 표시해야 한다. 이러한 셀들은 4-연결 이웃
으로 구현하여야 한다.


13. 포인터 Ⅱ

13. 포인터 Ⅱ, new와
delete

#include <stdio.h>

void f(int* ip) {

    (*ip)++;

}

void main() {

    int* ip;

    printf("Enter number:");

    scanf("%d",ip);

    f(ip);

    printf("%d\n",*ip);

}

  위 프로그램은 정수를 입력 받아, 이것을 함수 f()에 전달한다. 함수 f()에서
는 이 값을 1증가시킨 후, 리턴되어 main()에서 이 값을 출력한다. 만약 사용
자가 5를 입력했다면, 6이 출력될 것이다.

  자 답을 보지 말고, 이 프로그램의 오류를 수정해 보자.

  기억나는가? 변수는 사용하기 전에 항상 할당되어야 한다. 위의 경우, ip는
할당되었지만, ip가 가리키는 곳, 즉 정수를 위해서는 메모리가 할당되지 않았
으므로, 프로그램은 제대로 동작하지 않는다.

  ip가 가리키는 곳을 할당해야 한다. ip가 정수 1개를 가리킬 목적이라면, 2
바이트를 할당해야 할 것이다. 이것은 C의 표준 함수 malloc()■을 사용한다.

             C++의 new연산자가 도입되기 전 malloc()은 C의 표준 함수 중 굉장히 비
          중이 큰 함수였다. C++의 new연산자가 도입된 후 malloc()은 C++소스 코드
          에서 찾아볼 수 없게 되었다. 우리는 아직 new를 사용하지 않았으므로, 이
          장에서는 malloc()과 new를 조금씩 다룬다.

사용자에 의해 메모리의 크기와 할당의 시점이 정해지므로, 이러한 메모리 할
당을 동적 메모리 할당(dynamic memory allocation)이라 한다. 동적 메모
리 할당이 이루어지는 메모리 블락을 힙(heap)이라 한다. malloc()은 표준 헤
더 파일인 stdlib.h에 선언되어 있으므로 #include <stdlib.h>를 포함해야 한
다. malloc()의 처음 파라미터로 할당하고자 하는 메모리의 크기를 명시한다.
만약 1000바이트를 할당하기 원한다면,

malloc(1000);

처럼 사용한다. 또한 할당된 메모리는 해제해 주어야 하는데 표준 함수
free()를 사용한다. 수정된 소스는 아래와 같다.

#include <stdio.h>

#include <stdlib.h>

void f(int* ip) {

    (*ip)++;

}//f

void main() {

    int* ip;

    ip=(int*)malloc(2);//(int*)의 형 변환은 반드시 필요하다.

    printf("Enter number:");

    scanf("%d",ip);

    f(ip);

    printf("%d\n",*ip);

    free(ip);

}

  위의 소스에서 malloc()이 리턴하는 값에 대한 형 변환(type conversion)은
반드시 필요하다■. 이것은 포인터에 취해지는 연산의 결과가 바르게 동작함을

             malloc()은 사용자가 원하는 포인터 타입이 어떤 것인지 알 수 없다. 그렇
          다면 malloc()은 무엇을 리턴하는가? 모르는 포인터 타입을 리턴할 것이다.
          이것은 C에서 void*로 나타낸다. malloc()은 void*를 리턴한다. void*는 반드
          시 특정한 형의 포인터로 변환되어야 한다.

보장한다. 독자들은 char*와 int*의 차이점을 알고 있을 것이다. 위의 예에서
힙에 할당된 메모리는 정수이므로 정수를 가리키는 포인터 int*가 되어야 한
다. 만약 char*로 변환한다면, *ip는 정수 2바이트가 아닌 1바이트를 읽으려고
시도할 것이므로 정확한 값이 읽히지 않을 것이다. 위 프로그램의 메모리 상
태는 그리지 않는다. 독자들이 직접 그려보기 바란다.

  이제 형 변환의 중요성에 대해서 다시 한 번 살펴보자. malloc()을 사용해서
문자형 6바이트를 할당해서 각각을 1, 2, 3, 4, 5와 6으로 초기화하는 코드를
만들어 보자. 그리고 3을 출력한다.

#include <stdio.h>

#include <stdlib.h>

void main() {

    char* ip;

    ip=(char*)malloc(6);

    *ip=1;

    *(ip+1)=2;

    *(ip+2)=3;

    *(ip+3)=4;

    *(ip+4)=5;

    *(ip+5)=6;

    printf("%d\n",*(ip+2));

    free(ip);

}

  위의 소스에서 ip+2등은 제대로 된 메모리의 위치에 숫자를 대입한다. 만약
위의 소스에서

ip=(char*)malloc(6);

부분이

ip=(int*)malloc(6);

으로 바뀐다면, 어디가 잘못일까? 6바이트의 메모리 할당은 정수를 3개밖에
저장하지 못하므로, ip+3부터는 할당되지 않은 메모리를 접근하는 것이다. 그
러므로, 실행시 에러(run-time error)가 발생한다. 상기하자면,

ip+3

의 의미는

ip+3×sizeof(*ip)

이므로, ip에서 7번째 변위(offset)를 접근하므로 타당한 접근이 아니다.

  malloc()을 사용할 때 주의사항이 또 있다. 정수 3개를 할당하기 위해서

ip=(int*)malloc(6);

이 항상 올바르게 동작한다고 가정해서는 안 된다. 16비트 운영체제에서는 정
수가 2바이트이지만, Windows같은 32비트 운영체제에서는 정수는 4바이트이
므로, 위의 문장은 정수를 1.5개 할당할 뿐이다. 그러므로

ip=(int*)malloc(sizeof(int)*3);

처럼 수정되어야 한다. 위 코드가 32비트 운영체제에서 실행된다면, 12바이트
를 할당할 것이다.

  포인터의 포인터(pointer to pointer)

  포인터 변수를 가리키는 포인터를 선언할 필요는 없을까? 정수형 포인터 ip
를 선언하기 위해,

int* ip;

라고 선언한 것처럼, 정수형 포인터의 포인터 ipp를 선언하기 위해,

int** ip;

라고 선언한다. 이것은 ipp가 int*에 대한 포인터, 즉 정수를 가리키는 포인터
의 포인터 임을 의미한다. 아래의 예를 참고하자.

#include <stdio.h>

void main() {

    int i=2;

    int* ip;

    int** ipp;

    int*** ippp;

    ip=&i;

    ipp=&ip;

    ippp=&ipp;

    printf("%d,%d,%d,%d\n",i,*ip,**ipp,***ippp);

    printf("%p,%p,%p,%p\n",&i,ip,ipp,ippp);

    printf("%p,%p,%p,%p\n",&i,ip,*ipp,**ippp);

}

  이 예에서 ip는 i의 주소를 가리킨다. ipp는 ip의 주소를 가리킨다. ipp가
int** 형이므로 다음과 같은

ipp=&i;

문장은 불법(invalid)이다. ipp는 int**형이지만, &i는 int*형이기 때문이다.
ippp는 ipp의 주소를 가리킨다. 쓸 일은 거의 없지만 - 필자의 학부시절
Xenix의 소스 코드를 설명한 운영체제 책에서 본 일이 있다 - 이것은 포인터
의 포인터의 포인터를 의미한다. 위 프로그램의 메모리 상태는 다음과 같다.

             포인터의 포인터의 포인터: i=2이, &i=[2028]이다. ip는 i의 주소를 가리키므
          로, ip=[2028]이다. ip를 한 번 재참조(dereference)한 *ip는 [2028]이 가리키는
          곳, 즉 2를 의미한다. ipp=&ip이므로 [2024]이다. *ipp는 ipp가 가리키는 곳
          즉 [2028]을 의미한다. **ipp는 ipp가 가리키는 곳이 가리키는 곳, 즉 정수 2
          를 의미한다. ippp는 &ipp이므로 [2020]이다. ***ippp는 ippp가 가리키는 곳이
          가리키는 곳이 가리키는 곳, 즉 정수 2를 의미한다.

  포인터 변수가 선언된 이후 *는 재참조(dereference) 연산자(간접지정
(indirect)연산자)임을 주의하자. *ip는 ip가 가리키는 대상, 즉 정수를 의미한
다. *ipp는 ipp가 가리키는 대상, 즉 정수형 포인터를 의미한다. 그러므로,
**ipp는 *ipp가 가리키는 대상, 즉 정수를 의미한다. 이것은 계속해서 확장된
다.

  볼런드 C++ 4.5에서의 실제 실행 결과는

    2,2,2,2

    33B7:2028,33B7:2028,33B7:2024,33B7:2020

    33B7:2028,33B7:2028,33B7:2028,33B7:2028

이다■. 지역 변수이므로 메모리의 높은 곳에서 낮은 곳으로 할당된 것을 알

             주소의 출력 형태는 조금 특별하다. 이것은 16비트 시절의 세그먼트
          (segment)에서 비롯한다. 비록 Borland C++ 4.5가 32비트 Windows에서 동
          작하기는 하지만, 프로젝트에서 별도로 지정하지 않으면 16비트 코드가 생
          성된다. 이것은 Windows의 리얼 모드(real mode)로 알려져 있다. 실행 코
          드는 코드 세그먼트(code segment)와 데이터 세그먼트(data segment)등 몇
          개의 세그먼트로 분리되어 메모리에 로드된다. 주소 출력에서 콜론(:)의 앞
          부분은 세그먼트의 시작을 나타낸다. 위의 예에서 스택 세그먼트의 시작
          주소는 20비트 주소 0x33B70이다. 콜론의 뒷 부분은 세그먼트의 시작에서
          의 변위(offset)이다. 변수 i의 변위는 0x2028이다. 이 주소는 실제로 20비트
          주소(16비트 시절 메모리의 최대 크기는 1Mega였다) 0x33B70 + 0x2028 =
          0x35B98로 번역된다.

수 있다(이것은 사실이지만, 앞으로 정확히 명시해야 하는 경우를 제외하곤,
낮은 곳에서 높은 곳으로 할당되는 것처럼 메모리를 그릴 것이다).

  비주얼 C++ 5.0 혹은 C++ 빌더 3.0에서의 실제 실행 결과는

    2,2,2,2

    0064FDEC,0064FDEC,0064FDE8,0064FDF0

    0064FDEC,0064FDEC,0064FDEC,0064FDEC

이다■.

             비주얼 C++ 5.0은 32비트 코드를 생성한다. Windows는 32비트 선형 주소
          를 사용하므로, 16진수 8자리로 주소가 출력된다. 메모리 모델에 관한 설
          명은 "직접 메모리 접근"을 참고하라.

  위의 예에서 비록 '포인터의 포인터의 포인터'를 사용하기는 했지만, 실제
프로그래밍 시에 이러한 변수를 사용할 일은 거의 없다. 하지만, '포인터의 포
인터'는 가끔 사용되며, 실수하기 쉬운 부분을 포함하고 있다. 아래의 예에서
논리적 에러(semantic error)를 수정해 보라. 이 프로그램은 IntAlloc()에서 정
수 메모리를 할당한다. 이것은 main()에서 사용되며, IntFree()에서 할당된 정
수 메모리를 해제한다.

#include <stdio.h>

#include <stdlib.h>

void IntAlloc(int* ip) {

    ip=(int*)malloc(sizeof(int));

}//IntAlloc

void IntFree(int* ip) {

    free(ip);

}//IntFree

void main() {

    int* ip;

    IntAlloc(ip);

    *ip=5;

    printf("%d\n",*ip);

    IntFree(ip);

}

  어느 부분이 에러인지 발견했는가? 먼저 프로그램의 메모리 상태를 그려보
자.

             (a) main()에서 정수형 포인터 변수 ip의 공간을 스택에 할당한다. 이 변수
          는 초기화되어 있지 않으므로 메모리의 ?곳을 가리키게 된다. (b) 함수
          IntAlloc()을 호출한다. ip의 값이 복사되어 전달되므로, IntAlloc()의 ip역시 처
          음에는 ?의 곳을 가리키게 된다. (c) malloc()의 호출에 의해 힙에 2바이트
          의 메모리 공간이 할당된다. 이 곳을 [1000]이라 하자. 이제 ip는 [1000]을
          가리키게 된다.

  정수 i의 값을 바꾸기 위해서는 함수에 i를 전달하는 것이 아니라, i의 주소
를 전달해야 하는 것을 기억하는가? 위의 예에서 포인터 변수 ip을 값을 함수
IntAlloc()에서 바꾸어야 하므로, main()에서 IntAlloc()을 호출할 때, ip의 포
인터를 전달해야 한다. ip가 정수형 포인터 int*이므로, IntAlloc()에는 정수형
포인터의 포인터 int**를 전달해야 하는 것이다.

             IntAlloc()을 빠져 나오면, [1000]을 가리키던 ip가 메모리에서 해제(팝)된다.
          문제는 2가지다. 할당된 [1000]을 가리키는 포인터가 없으므로, 메모리 샘
          (memory leak)이 발생한다. main()의 ip는 여전히 할당되지 않은 메모리의
          곳 ?를 가리키므로, *ip를 사용하는 것은 불법이다.

  수정된 소스는 아래와 같다.

#include <stdio.h>

#include <stdlib.h>

void IntAlloc(int** ip) {

    *ip=(int*)malloc(sizeof(int));

}//IntAlloc

void IntFree(int* ip) {

    free(ip);

}//IntFree

void main() {

    int* ip;

    IntAlloc(&ip);

    *ip=5;

    printf("%d\n",*ip);

    IntFree(ip);

}

  위 프로그램의 메모리 상태는 아래와 같다.

             (a) main()의 ip가 스택에 할당되었을 때 ip는 쓰레기(garbage)로 초기화된
          다. (b) main()에서 함수 IntAlloc()을 호출할 때 ip의 주소를 전달한다. 그러
          므로, IntAlloc()에는 [2000]이 전달되며 파라미터는 int**로 선언되어야 한다.
          이 변수는 아마도 1996번지에 할당될 것이다. IntAlloc()에서 ip는 main()의
          ip, 즉 [2000]을 가리킨다. (c) IntAlloc()에서 동적으로 메모리를 할당했을 때
          1000번지가 할당되었다고 하자. ip가 가리키는 곳(*ip)에 [1000]을 대입한다.
          [2000]의 ip가 [1000]을 가리키게 된다.

  정수의 값을 바꾸기 위해서는 정수의 포인터를 전달해야 하듯이, 포인터를
바꾸기 위해서는 포인터의 포인터를 전달해야 한다.

             main()에서 ip가 가리키는 [1000]의 곳(*ip)에 5를 대입한다. 이것은 타당한
          메모리 접근이므로 에러가 발생하지 않는다.

참조(reference)

  실제로 이 문제(포인터를 바꾸기 위해 포인터의 포인터를 전달하는 문제)는
C++의 새로운 참조(reference) 기능에 의해서도 해결 가능하다. 참조를 사용한
소스는 아래와 같다.

#include <stdio.h>

#include <stdlib.h>

void IntAlloc(int*& ip) {

    ip=(int*)malloc(sizeof(int));

}

void main()

{

    int* ip;

    IntAlloc(ip);

    *ip=5;

    printf("%d\n",*ip);

    free(ip);

}

  수정되기 전의 소스에서 바뀐 부분이

void IntAlloc(int*& ip) {

뿐임에 주목하라. C에서는 정수 i를 바꾸기 위해서 f(int* ip)식으로 i의 주소
를 전달했다. 정수 포인터 ip를 바꾸기 우해서 f(int** ip)식으로 ip의 주소를
전달했다. C++에서 정수 i를 바꾸기 위해서 f(int& i)식으로 i의 참조를 전달
할 수 있다. 실제로 이 문장은 i의 주소를 전달한다. 하지만, 사용자가 해 주어
야 하는 것은 단지 함수의 선언에서 참조로 전달하고자 하는 변수의 앞에 &■

             &는 비트곱 연산자(bitwise AND operator), 주소 연산자(address-of
          operator) 그리고 참조(reference)로 쓰인다.

를 붙이는 것뿐이다. 그러므로 정수 포인터를 바꾸기 위해서 f(int*& ip)처럼
사용한다.

  참조에 관한 주제는 "참조와 별명"에서 자세히 다룬다.

  가용 공간 리스트(available list): 진보된 주
제

  malloc()의 동작을 자세히 살펴보면 신기한 부분을 포함하고 있다. 아래의
소스에서 이상한 부분을 찾아 보라.

#include <stdio.h>

#include <stdlib.h>

void main()

{

    int* ip;

    int* ip2;

    ip=(int*)malloc(sizeof(int)*2);

    ip2=(int*)malloc(sizeof(int)*4);

    ip[0]=1;

    ip[1]=2;

    ip2[0]=3;

    ip2[1]=4;

    ip2[2]=5;

    ip2[3]=6;

    printf("%d,%d\n",ip[1],ip2[2]);//2와 5가 출력된다.

    free(ip);//ip만으로는 ip가 가리키는 곳이 4바이트인지 알 수 없다.

    free(ip2);

}

  위의 소스는 ip가 정수 2개(4바이트)를 가리키고, ip2가 8바이트를 가리키게
하고 있다. 그런 다음 내용 연산자 []를 사용하여, 각각을 1에서 6으로 초기화
한다. 이 변수를 적당하게 사용한 뒤, ip와 ip2를 해제한다. 의문은 다음과 같
다.

"free()함수가 ip와 ip2를 해제할 때 ip는 4바이트의 곳을 가리키며, ip2는
8바이트의 곳을 가리키는지 어떻게 알 수 있는가?"

  이것은 C컴파일러가 사용 가능한 메모리 블락(available memory block)
에서 메모리를 할당하는 방식에 기인한다. 사용자가 10바이트의 메모리 블락
을 할당하기 위해 malloc(10)을 호출하면, CRT 힙 관리자(heap manager)는
사용 가능한 메모리가 있는지 검사한 후, 없으면 NULL을 리턴한다. 있다면,
크기 정보 4바이트를 포함한 14바이트의 메모리 블락을 할당한 후(이 블락의
주소를 1000이라 하자) 4바이트를 건너뛴(skip) 곳의 주소를 리턴한다(1004가
리턴된다). 사용 가능한 메모리 블락은 14바이트가 줄어 든다.

             malloc()이 할당하는 메모리 블락: 크기 정보 4바이트를 포함한 크기를 메
          모리에서 할당한다. 사용자는 이 포인터를 이용해서 연산을 할 수 있어야
          하므로, 리턴되는 주소는 헤더의 주소가 아니라, 헤더+4의 주소가 리턴된
          다. free()에 주소를 넘겨주면 free()함수는 '파라미터 주소-4'를 계산하여 실
          제 할당된 메모리의 크기를 구한다. 그런 다음 이것을 해제한다. 메모리
          블락은 컴파일러에 의존적이다. 다른 C컴파일러는 다른 구조의 메모리 블
          락을 사용할 수도 있으며, 이 예는 볼런드 DOS 메모리의 경우를 필자가
          짐작해서 그린 것이다. farmalloc()인 경우 헤더의 크기는 (아마도) 8바이트
          이다. 이것은 free()와 farfree()가 호환되지 않는 이유이다.

  이것이 ip[1], ip2[2]가 제대로 할당된 메모리를 참조하는 이유이다. 또한
free()는 메모리의 크기를 계산하기 위해 '파라미터로 넘어온 주소-4'를 계산
하여 크기를 얻는다. 그리고 이 부분을 힙에서 해제한다. farmalloc()과
farfree()는 다른 크기의 헤더를 사용하므로, malloc()과 farmalloc()은 호환되
지 않는다. 또한 malloc()은 4바이트의 헤더를 사용하므로, 64K바이트 - 도스
에서 세그먼트(segment)로 알려진 - 이상의 메모리는 할당할 수 없다.

             ip가 [5000]에 ip2가 [4996]에 할당되었다고 하자. 첫 번째 malloc()호출은 4
          바이트를 할당하려 시도한다. 사용 가능한 힙의 주소가 [1000]이었다면, 헤
          더 4바이트가 블락의 크기 4로 초기화되면서, 모두 8바이트(헤더 4바이트+
          데이터 4바이트)가 할당된다. 그리고 malloc()은 [1000]이 아닌 [1004]를 리턴
          한다. 두 번째 malloc()호출도 마찬가지이다. 그러므로, 메모리가 2000바이
          트 남았을 때, 동적으로 1000개의 정수를 할당할 수 있을 것이라고 가정해
          서는 안된다. 2000/4=500개의 정수밖에 할당할 수 없다. 이것은 메모리 구
          조를 이해하지 못한 초보자들이 만든 데이터베이스 응용 프로그램(메모리
          에 데이터를 유지하는)에서 간혹 찾아 볼 수 있다. 앞으로 삽입 가능한 레
          코드가 100개로 보고(report)되어도, 실제로는 60여개를 삽입하려하면, 실행
          시 에러가 발생한다.

  실제로 힙에서 사용 가능한 메모리 공간은 어떻게 유지되는 것일까?
malloc()호출이 되었을 때, 어떻게 사용 가능한 메모리 공간을 힙에서 찾을 수
있을 까(가용 공간 리스트(av-list)가 유지된다)? 차례대로 할당한 메모리 공
간이 차례대로 해제되지 않을 때 발생하는 띄엄띄엄한 메모리 블락 문제(단편
화(fragmentation)라고 한다)는 어떻게 해결할까? 이것은 '메모리 모델'에서
자세히 다룬다.

  실제 여분의 4바이트가 할당되는지 살펴보기 위하여 C의 함수 coreleft()를
사용해 보자. 볼런드 컴파일러에서 coreleft()는 alloc.h에 선언되어 있다. 이것
은 DOS의 사용 가능한 메모리를 바이트 단위로 리턴한다.

#include <stdio.h>

#include <stdlib.h>

#include <alloc.h>

void main()

{

    int* ip;

    int* ip2;

    unsigned long size0,size1,size2;

    size0=coreleft();

    ip=(int*)malloc(sizeof(int)*30);

    size1=coreleft();

    ip2=(int*)malloc(sizeof(int)*8);

    size2=coreleft();

    printf("%d\n",sizeof(int));

    printf("%ld,%ld,%ld\n",size0,size0-size1,size1-size2);

    free(ip);

    free(ip2);

}

  결과는

    2

    561280,64,32

이다. 필자의 컴퓨터는 메모리가 64Mega임에도 불구하고, 사용 가능한 메모
리가 561,280으로 찍힌 이유는 coreleft()가 도스 전용 함수이기 때문이다. 정
수 30개(60바이트)를 할당하기 전의 메모리에서 60바이트를 할당한 후의 메모
리 차이는 64이다. 여분의 4바이트가 더 할당된 것을 알 수 있다.

  2번째 결과는 조금 이상하다. 16바이트를 할당하면, 20바이트가 할당되어야
하는데, 32바이트가 할당되어 있다(항상 16의 배수 - 도스에서 패러그래프
(paragraph)로 알려진 - 로 할당된다). 이 사항은 '메모리 모델'에서 자세히
다룬다.

  지금까지 malloc()에 관한 장황한 설명을 하였음에도 불구하고, 필자가 C++
로 프로그램을 코딩한 이후로 malloc()을 사용한 적이 없다. malloc()보다 막
강한 new와 delete가 C++의 연산자에 추가되었기 때문이다!

  new와 delete

  new와 delete는 동적으로 메모리를 할당하는 C++의 새로운 연산자이다. 하
지만, new/delete는 다음과 같은 점이 malloc() 부류의 함수보다 막강하다.

    ■ 명시적인 형 변환이 필요없다.

    ■ malloc()은 함수지만, new는 연산자(operator)이다.

    ■ 연산자이기 때문에 유연함을 제공한다.

  new의 문법은 다음과 같다.

    [::] new [new-args] type-name [initializer]

    [::] new [new-args] ( type-name ) [initializer]

  []기호가 선택사항(option)을 나타낸다는 것을 주의하자. 예를 들어, 정수 크
기의 메모리를 할당하여, ip가 이곳을 가리키도록 하려면

    int* ip;

    ip=new int;

처럼 사용한다. 이것은

    #include <alloc.h>

    int* ip;

    ip=(int*)malloc(sizeof(int));

와 동일하다. 크기를 지정하는 sizeof(int)가 int로 바뀐 점과 malloc()의 리턴
값을 형 변환한 (int*)가 없어진 점에 유의하라. new는 연산자이므로, 명시적
인 형 변환이 필요 없다■.

             이것을 형 안정(type safety)이라고 한다.

  delete의 문법은 다음과 같다.

    [::] delete pointer

    [::] delete [ ] pointer

  위의 예에서 ip가 가리키는 할당된 메모리를 해제하기 위해서는

delete ip;

처럼 사용한다.

  정수(integer) 크기의 메모리를 힙에서 할당하기 위해(int* ip;가 선언되었다
고 가정하자) 다음과 같은 new연산자를 사용할 수 있다.

ip=new int;

  new 다음에 형 이름을 적는다. 위의 경우 형 이름이 int이므로 int크기만큼
을 힙에서 할당한다. 이곳을 ip가 가리키게 된다. 형 이름은 괄호로 묶여도 된
다.

ip=new (int);

위의 문장이 가능하지만, 괄호는 생략하는 것이 일반적이다(형의 이름이 길다
면 모호함을 피하기 위해 괄호를 해 주는 것이 좋다). 또한 new 앞에 범위
해결사(scope resolver) ::를 붙여도 된다.

ip=::new int;

이것은 후에 설명할 오버로딩된 클래스의 멤버 연산자 new와 원래(original)
new를 구분 짓기 위해 가끔 사용된다(이것은 'new/delete: 진보된 주제'에서
다룬다). 하지만, 아직은 클래스가 사용되지 않았으므로, ::은 붙이지 않는 것
이 일반적이다.

  메모리 할당 후 값을 초기화하는 new문장을 사용할 수 있다. 이것은 위에
서 적은 문법에서 initializer로 표현된 것이다. 예를 들어 정수를 할당한 후
이 값을 3으로 초기화시키기 위해서는

    ip=new int;

    *ip=3;

대신에

ip=new int(3);

처럼 사용할 수 있다. 위의 문장이 실행된 후 *ip는 3이다.

  지금까지 예로 든 모든 경우, 힙에서 할당된 메모리를 해제하기 위해서는

delete ip;

를 사용한다. 포인터가 가리키는 대상의 크기가 알려지지 않은 것이 궁금하지
만, malloc()에서 언급한 것과 같은 사항이 적용되므로 delete는 올바르게 동
작한다. 아래의 예와 메모리 상태를 참고하라.

#include <stdio.h>

void main() {

    int* ip;

    ip=new int(3);

    printf("%d\n",*ip);

    *ip=5;

    printf("%d\n",*ip);

    delete ip;

}

결과는

3

5

이다.

             (a) int* ip; (b) ip=new int(3); (c) *ip=5;

  malloc()을 이용해서 10개의 정수를 위한 메모리를 할당하기 위해

ip=(int*)malloc(sizeof(int)*10);

을 사용할 수 있다. 이렇게 할당한 후에, ip[0], ip[1], ... ,ip[9]의 10개의 정수
를 사용할 수 있다. 이것을 new로는 다음과 같이 한다.

ip=new int[10];

메모리는 10×sizeof(int), 즉 20바이트가 할당된다. ip는 할당된 20바이트의 시
작을 가리킨다. 또한, 5번째 정수를 참조하기 위해 ip[4]를 사용할 수 있다. 독
자들은

ip=new int(10);

과

ip=new int[10];

을 구분해야 한다. 전자의 경우 2바이트를 할당하고 10으로 초기화는 문장이
지만, 후자의 경우, 20바이트(정수 10개의 배열)를 할당한다. 그러므로 어떤 형
(type)의 변수 n개만큼의 메모리를 확보하기 위해서는

new type[n];

처럼 사용한다. 이렇게 기본형의 배열로 초기화되었을 경우, 메모리를 해제하
기 위해서는

delete[] ip;

처럼 해야한다. delete ip; 처럼 사용하는 것은 오류라는 사실을 잊지말라. 이
것은 malloc()과 farmalloc()이 다른 메모리 블락을 사용하므로, 각각에 대해
free(), farfree()를 사용해야 하는 것과 같은 이유이다.

  아래에 전체적인 예를 리스트하였다.

#include <stdio.h>

void main() {

    int* ip;

    int* ip2;

    int* ip3[2];

    ip=new int;

    ip2=new int[5];

    ip3[0]=new int[5];

    ip3[1]=new int;

    *ip=5;

    printf("%d\n",*ip);

    delete ip;

    delete[] ip2;

    delete[] ip3[0];

    delete ip3[1];

}

  정수 포인터 배열이 선언된 경우, new를 어떻게 사용하여 메모리를 할당했
느냐에 따라,

delete[] ip3[0];

혹은

delete ip3[1];

로 메모리를 해제했다는 사실에 주의하라.

  기본형의 배열로 메모리를 할당한 경우, 초기화 할 수 없다. 예를 들어 크기
5인 정수배열을 할당해, 1,2,3,4와 5로 초기화하기 위한, 아래의 문장은 그럴듯
해 보이지만, 모두 에러이다.

    ip=new int[5](1,2,3,4,5);

    ip=new int[5]{1,2,3,4,5};

    ip=new int(1,2,3,4,5)[5];

    ip=new int{1,2,3,4,5}[5];

  그렇다면 다음의 new문장은 어떤가?

ip=new(1,2,3,4,5) int[5];

  이것은 역시 에러지만, new가 적절히 오버로드(overload)되었다면, 에러가
아니다. 이것은 'new/delete: 진보된 주제'에서 다룬다. 실제 위의 문장에서
괄호 (1,2,3,4,5)는 new문법의 new-args에 해당한다.

2차원 배열의 할당

  new를 사용하여 2차원 이상의 배열을 할당하는 경우는 어떻게 할까? 아래
의 예를 참고하라. 아래의 예는 4×10의 2차원 문자 배열을 할당하여, pchar가
가리키도록 하고 있다. pchar의 선언에 주목하라. pchar의 선언이

char* pchar[10];

이면 이 프로그램은 컴파일되지 않는다. 이것은

char* pchar[10];

과

char (*pchar)[10];

이 다르다는 것을 의미한다. 자세한 사항은 'new/delete: 진보된 주제'를 참
고하라.

#include <stdio.h>

#include <string.h>

void main() {

    int dim=4;

    char (*pchar)[10];//배열이 선언된 것이 아니라, pchar이 선언된 것이

                       //다.

    pchar=new char[dim][10];

    strcpy(pchar[0],"gold");

    strcpy(pchar[1],"silver");

    strcpy(pchar[2],"copper");

    strcpy(pchar[3],"neck");

    printf("%s\n",pchar[0]);

    delete[] pchar;

}

  출력 결과는

gold

이다.

  실습문제

1.


14. typedef, enum

14. 형을 정의하는 법:
typedef, enum

  사용자가 필요로 하는 형(type)을 정의(definition)할 수 있는 방법을 C++는
제공한다. 형을 정의할 때는 이미 정의된 형을 이용할 수도 있고, 전혀 새로운
형을 정의할 수도 있다. 또한 C++의 구조체(structure), 공용체(union)와 클래
스(class)는 동적인 형(dynamic type) - 형이 정적인 변수뿐만 아니라, 함수도
포함한다 - 을 정의하는 기능을 제공한다. 형을 정의하는 방법은 다음과 같은
것이 있다.

    ■ typedef

    ■ enum

    ■ struct

    ■ union

    ■ class

  이 장에서는 typedef와 enum에 대해서 다루고, 다음 장에서 struct와 union
에 대해서 다룬다. class는 'C++의 구조체'에서 다룬다.

  typedef(TYPE DEFinition)

  typedef는 형을 정의하는 C++의 공식적인(?) 키워드이다. 하지만 typdef가
새로운 형을 정의할 수는 없다. 이미 존재하는 형의 별명을 정의하는 것이다.
그렇다고 typedef를 #define과 같은 정도로 격하시켜서는 안 된다. typdef는
#define보다 막강하다.

  unsigned char는 다른 언어 - 파스칼 같은 - 에서 바이트(byte)라고 부른
다. 이제 unsigned char형과 같은 byte형을 정의해 보자. typedef를 이용하여
byte를 아래와 같이 정의한다.

typedef unsigned char byte;

  typedef는 형을 정의하는 문장(statement)이므로 끝에 세미콜론 ;이 필요하
다. 마지막의 단어가 형으로 정의된다는 것을 주의하라. 예를 들어

typedef unsigned int WORD TYPE;

은 unsigned int WORD를 TYPE으로 정의하려하기 때문에 컴파일 시간 에러
가 발생한다. 만약 정의하려는 형이

typedef char byte;

라면 이것은

#define byte char

과 같은 역할을 한다. 물론 typedef는 byte라는 완전한 형으로 동작하지만,
#define으로 정의한 byte는 char로 매크로 확장되는 것이다. 하지만, 사용자가
그 차이를 구분할 수는 없다. 하지만,

typedef unsigned char byte;

를

#define byte unsigned char

로 정의할 수는 없다. 이 경우 #define은 문법적 에러■이다.

             실제 이것은 전처리 시간 에러(preprocessing time error)이다. 하지만, 일반
          적으로 이러한 에러도 컴파일 시간 에러(compile time error)로 취급한다.

  길이가 40인 문자열을 관리하기 위해 41바이트의 문자열 배열을 형으로 정
의할 수 없을까? 아래와 같이 str40이라는 형을 정의할 수 있다.

typedef char str40[41];

  위의 경우 str40이 형 이름(type name)이라는 것을 주의하라. 배열이라는
특이성 때문에 첨자가 형의 이름 뒤에 위치하지만, 형의 이름은 str40이다. 그
러므로

str40 s;

는 크기가 41인 문자 배열 s - char s[41]처럼 - 를 선언한 것이다.

  좀 특별한 형 정의는 함수 포인터 형(function pointer type)을 정의하는 경
우에 또 발생한다. 이것은 '함수 포인터'에서 자세히 다룬다.

typedef long (*Fun)(long,long);■

             이 경우 형의 이름은 Fun이다. 이것은 긴 정수 2개를 파라미터로 받고 긴
          정수를 리턴하는 함수 포인터 형이다. 만약 typedef가 생략되면 long
          (*Fun)(long,long); 처럼 되는데, 이 경우 Fun은 변수가 된다. 즉 typedef가
          있느냐, 없느냐에 따라 형 혹은 변수가 되는 것이다.

  아래의 예를 참고하라.

#include <stdio.h>

#include <conio.h>

#include <string.h>

typedef unsigned char byte;

typedef char str40[41];

typedef int (*Fun)();

void main() {

    byte i=65;//unsigned char i=65;와 같다.

    str40 s;//char s[41];와 같다.

    Fun f;//int (*f)();와 같다.

    strcpy(s,"When I was a little boy");

    f=getch;//getch()의 시작 주소를 f에 대입한다.

    printf("%c,%s\n",i,s);

    (*f)();//f가 가리키는 곳, 즉 getch()를 호출한다.

}

  결과는

A,When I was a little boy

이다.

범위(scope)

  typedef등으로 정의한 형이 보이는 범위는 변수와 같다. 위의 예에서
typedef가 main()안에서 정의되었다면, main()안에서만 볼 수 있다. 아래의 예
는 컴파일 시간 에러가 발생하는 예이다. 에러를 수정해 보라.

#include <stdio.h>

#include <conio.h>

#include <string.h>

typedef char str40[41];

typedef int (*Fun)();

int f();

void main() {

    typedef unsigned char byte;

    byte i=65;

    str40 s;

    Fun f;

    strcpy(s,"When I was a little boy");

    f=getch;

    printf("%c,%s\n",i,s);

    (*f)();//여기가 에러일까?

}

int f() {

    static byte c=0;//여기가 에러일까?

    ++c;

    return c;

}

  enum: 열거형

  enum은 열거형(ENUMeration type)을 정의할 수 있고, 열거형 상수를 정
의할 수도 있다■. 또한 열거형 변수를 선언할 수도 있다.

             enum은 클래스를 정의할 때, 상수를 정의할 수 있는 유일한 키워드이다.
          const, static 모두 클래스의 내부에서 변수를 초기화할 수 없다.

  enum의 문법은 다음과 같다.

enum [<tag_name>] {<constant_name> [= <value>], ...} [var_list];

  []가 선택사항을 나타낸다는 사실을 주의하라. <>는 사용자가 지정해야 하
는 명칭(identifier)을 나타낸다. 문법에서 보듯이 tag_name은 생략될 수 있고,
상수 이름의 값 <value> 역시 생략할 수 있으며, 상수 이름
<constant_name> 은 반복할 수 있다. 마지막의 변수 리스트 var_list 역시 생
략할 수 있다■.

             앞으로 계속 되는 장에서, 문법을 나타내기 위해 이러한 표기법을 사용한
          다.

  브레이스 {}안의 상수는 명시적인 초기화가 없으면, 0부터 차례대로 초기화
된다. 아래의 문장은 sun부터 sat까지를 0부터 6까지 초기화한다.

enum {sun, mon, tue, wed, thu, fri, sat};

이것은 다음과 같이 명시적인 초기화를 할 수도 있으며,

enum {sun=0, mon=1, tue=2, wed=3, thu=4, fri=5, sat=6};

아래와 같은 #define 매크로 문장과 기능이 동일하다.

#define sun	0

#define mon	1

#define tue	2

#define wed	3

#define thu	4

#define fri	5

#define sat	6

  몇 가지 예를 들어보면,

    ■ enum {FALSE, TRUE} b;

  FALSE와 TRUE 상수를 선언하고, 변수 b를 선언한다. b는 FALSE 혹은
TRUE값을 가질 수 있다.

    ■ enum BOOLEAN {FALSE2, TRUE2} b2,b3;

  FALSE2와 TRUE2 상수를 선언하고, BOOLEAN 형을 정의했으며,
BOOLEAN형의 변수 b2,b3을 선언했다. BOOLEAN이 형으로 정의되었으므
로, BOOELAN i,j; 처럼 변수 선언을 할 수 있다. b2,b3은 FALSE2, TRUE2
값을 가질 수 있다.

    ■ enum BOOLEAN2 {FALSE3, TRUE3};

  변수는 선언하지 않고 FALSE3과 TRUE3을 상수로 선언했으며,
BOOLEAN2 형을 정의했다. BOOLEAN2 i,j; 처럼 변수 선언을 할 수 있다.

  아래의 전체적인 예를 참고하라.

#include <stdio.h>

enum {sun, mon, tue, wed, thu, fri, sat};

enum {FALSE, TRUE} b;

enum BOOLEAN {FALSE2, TRUE2} b2,b3;

enum BOOLEAN2 {FALSE3, TRUE3};

void main() {

    char str[][6]={"false","true"};

    b=FALSE;

    b2=FALSE2;

    b3=TRUE2;

    BOOLEAN2 bValue;

    bValue=FALSE3;

    printf("%s,%s,%s,%s\n",str[b],str[b2],str[b3],str[bValue]);

    printf("%d,%d,%d\n",sun,sat,TRUE);

}//main

결과는

    false,false,true,false

    0,6,1

이다.

  enum의 상수값의 초기화가 생략되었을 때, 상수 값은 '이전값+1'로 초기화
된다. 아래의 예에서 B,C,BB와 BBB는 각각 2,3, 11과 101로 초기화된다.

enum {A=1, B, C, AA=10, BB, AAA=100, BBB};

컴파일러의 입장

  enum형의 변수가 enum에서 정의한 상수 이외의 값을 가질 수 있을까?

    enum BOOLEAN {FALSE, TRUE};

    BOOLEAN b;

처럼 선언되었을 때, b에는 어떤 값을 대입할 수 있을까? 물론 FALSE와
TRUE를 대입할 수 있으며, 각각은 정수 0과 1과 같다. 그렇다면 b에 100을
대입하는 다음의 문장은 어떤가?

b=100;

위의 문장은 에러가 아니다. 실제로 컴파일러는 enum형을 정수형과 동일하게
취급한다. 그러므로, 정수값 100을 대입할 수 있으며, sizeof(BOOLEAN)의 결
과는 정수의 크기와 같은 2이다.

  하지만, b=100; 같은 문장은 피하는 것이 좋다. enum형은 특별히 열거된 상
수 값만을 가지도록 선언된 변수이다. 프로그래머의 의도가 b에 TRUE 혹은
FALSE를 대입할 목적이었으므로, 그렇게 사용해야 하는 것이다■.

             파스칼(Pascal) 언어에서도 enum형을 정의한다. 하지만, 파스칼은 enum형
          과 integer형이 호환되지 않는다. enum형의 변수에는 정확히 enum에서 정
          의한 상수값만으로 초기화해야 한다. 파스칼은 enum과 관계된 in이라는 집
          합 연산자도 제공한다. 하지만, C는 유연성을 위해 enum과 int는 같다.

  클래스에서 enum의 사용

  아래의 예제는 클래스 안에서 선언된 const, static과 enum상수를 초기화하
는 방법을 보여주고 있다. 아직 클래스에 대해 다루지 않았으므로, 아래의 예
는 건너뛰어도 좋다. 하지만, 표시하여 두었다가, 정적 변수(static variable),
상수(constant)와 클래스(class)를 모두 본 뒤 다시 이 곳을 보기 바란다.

#include <iostream.h>

#include <stdio.h>

class CTest {

public:

    const int c;

    enum { A=100, B=200 };

    int i;

    static int s;

public:

    CTest(int t=0):c(10) {

        i=t;

    }

    void Print() {

        printf("i=%d\n",i);

        printf("c=%d\n",c);

        printf("s=%d\n",s);

    }

};//class CTest

int CTest::s=20;

void main() {

    CTest c;

    c.Print();

    printf("%d\n",CTest::A);

}//main

  위의 예에서처럼 클래스 안에서 enum 상수를 선언하여 쓰는 것은 일반적이
다. C++ 표준 스트림(stream) 라이브러리인 iostream도 많은 enum상수를 포
함하고 있다.

  실습문제

1.


15. 구조체, 공용체

15. 구조체(structure), 공
용체(union)

태초에 구조체(structure)가 있었다. 구조체는 소진화(microevolution)를
반복하여, 클래스(class)로 진화하였다. 태초에 정수(integer)가 있었다.
정수는 아무리 소진화를 반복해도 클래스가 될 수 없었다■.

             소진화와 대진화에 대해서는 http://www.kacr.or.kr을 참조하라.

  우리는 이전 장에서, 같은 형(same type)의 변수를 여러 개 선언하기 위해
배열을 사용하는 것을 배웠다. 이 장에서는 다른 형의 변수(필드 이름(field
name))를 1개의 대표이름(구조체(structure) 변수 이름)으로 참조하는 방법에
대해서 배운다.

  이 장의 주제는 매우 중요하다. 독자들은 다소 어려워지는 포인터에 관한
주제를 다시 접할 것이며, 구조체를 파라미터로 전달하는 효과적인 방법을 배
울 것이다. 또한 구조체가 함수의 파라미터로 전달되거나, 함수가 구조체를 리
턴할 때 생기는 복사 문제에 대한 해결책에 대해서도 잠시 언급하게 될 것이
다.

  또한, 구조체의 필드 - 이제는 멤버(member)라고 불리는 - 를 접근하기 위
한 새로운 2개의 연산자 . 와 -> 에 대해서도 배우게 될 것이다.

  하지만, 구조체가 실제 중요한 이유는, C++에서 추가된 진보된 기능 때문이
다. 하지만, 이 사항은 'C++의 구조체'에서 다룰 것이다.

  또한 반드시 구조체 - 혹은 클래스 - 로 구현 가능한 몇 가지문제에 대해
서도 언급할 것이다.

  왜 이것이 필요한가?

  회원 관리 프로그램을 만든다고 생각해 보자. 회원 1사람을 나타내기 위해
서 독자들은 아마, 3개의 변수를 사용할지 모른다.

    char name[10];

    int age;

    long phone■;

             실제 프로그램에서는 전화번호를 관리하기 위해서 아마도 문자열 배열을
          사용하는 것이 훨씬 유동적일 것이다. 여기서는 설명을 위해 long을 사용
          한다.

  name, age 와 phone은 각각 회원의 이름, 나이와 전화번호를 나타내기 위
해 사용할 예정이다. 만약 이러한 회원이 100여명 정도 예상된다면, 프로그램
에서 아래처럼 변수 선언을 한다.

    char name[100][10];

    int age[100];

    long phone[100];

  위의 변수들을 사용해서 프로그램을 코딩할 수 없는 것은 아니지만, 다음과
같은 문제점이 존재한다.

    ① name[0], age[0]과 phone[0]이 연관되어 있다는 문법적 지시가 필

    요하다. 이것은 사소한 문제(not trivial)가 아니다. 왜 회원 1사람을

    나타내기 위한 3개의 변수가 서로 관련이 없는 것처럼 선언되어야 하

    는가!

    ② 100명 이상의 회원은 관리할 수 없다. 또한 회원수가 10명이라면,

    배열의 나머지 공간은 낭비된다.

  구조체(structure)는 이러한 2개의 문제점에 대해, 모두 해결책을 제시한다.
즉 구조체는 서로 다른 형을 가지는 여러 개의 변수들을 1개의 대표이름
으로 참조할 수 있는 문법적 구조를 제공한다. 또한, 구조체를 이용한 링크
드 리스트(linked list)는 메모리 관리 문제에 대해서도 해결책을 제시한다.

   문법

struct [tag-name] { 필드선언;[...] } [변수리스트];

  구조체는 키워드 struct로 시작한다. struct 다음에 구조체의 꼬리표 이름(태
그(tag) name)을 명시한다. 태그는 형을 정의하는 경우의 선택사항이므로,
생략해도 좋다(그래서 이것은 구조체 이름이 아니라 꼬리표 이름이다).

  구조체를 이루는 필드의 선언은 struct 다음의 연속하는 대괄호 사이에 필
드 수만큼 명시한다. 필드 선언 후에 변수들의 리스트가 올 수도 있다. 하지
만, 형 만을 정의하는 경우 변수 리스트는 생략 가능하다. 마지막에 문장의 끝
을 나타내는 세미콜론(;)은 반드시 명시되어야 한다. 이것은 구조체가 블락이
아니라, 문장이기 때문에 당연하다.

struct _PEOPLE■ { int age; char name[10]; long phone; } i;

             관례상 태그 이름은 언더스코어(_)로 시작한다.

  위의 예는 age,name과 phone을 필드로 가지는 새로운 구조체 형 struct
_PEOPLE을 정의(definition)한 것이다■. 또한 구조체 struct _PEOPLE 형의

             _PEOPLE을 정의한 것이 아니라, struct _PEOPLE을 정의한 것이라는 것
          을 주의하라.

변수 i를 선언(declaration)한 것이다. 태그 이름 _PEOPLE을 생략하거나, 변수
선언 i를 생략한 형태의 각각의 의미는 다음과 같다.

struct { int age; char name[10]; long phone; }   i;

                   ①                           ②

  위의 예에서는 태그 이름을 생략하였다. 그러므로 새로운 형은 정의되지 않
았으며, ①이 형 이름에 해당하고, ②가 변수 이름에 해당한다(변수를 선언하
기 위해서 형이름 변수이름; 형태의 문법을 사용한 것에 주목하라. 이것은
정수형 변수를 선언하기 위해 int i; 라고 사용한 것과 같은 문법이다).

struct _PEOPLE { int age; char name[10]; long phone; };

  위의 예에서는 태그 이름은 생략하지 않았고, 변수 이름은 생략한 형태이다.
이것은 struct _PEOPLE이라는 새로운 형을 정의(type definition)한 것이다.
이제 형이 정의되었으므로, 변수를 선언하기 위해서

struct _PEOPLE  i;

       ①       ②

라고 선언하는 것이 가능하다. 마찬가지로, ①이 형 이름에 해당하고, ②가 변
수 이름에 해당하는 타당한 변수 선언문장이다■. 이밖에도 typedef를 이용하

             프로그램의 확장자가 CPP라면, struct를 생략하는 것이 가능하다. 즉, C++
          컴파일러에서 struct _PEOPLE i;는 _PEOPLE i;와 같다. 하지만, 확장자가
          C라면, struct를 생략하면 컴파일 시간 에러가 발생한다. C++에서 struct를
          생략해도 _PEOPLE이 형 이름 역할을 하므로, _PEOPLE은 더 이상 태그
          이름이 아니라, 형 이름인 것은 사실이다. 하지만, 관례상 C++에서도 이것
          을 태그 이름이라 한다.

여 구조체 형을 정의하는 것과, 몇 가지 세부사항이 있지만, 'C++의 구조체'
에서 자세히 언급하므로, 이 장에서는 더 이상 언급하지 않는다.

  그렇다면, 일반적으로 프로그래머들은 어떤 형태를 많이 사용할까? 필자의
경우는 마지막 형식 - 태그 이름을 사용해서 형을 정의한 다음, 이 형의 변수
를 다른 줄에 선언하는 방식 - 을 많이 사용한다. 또한 대부분의 프로그래머
들도 마지막 형식을 많이 사용한다. 이것은 '형을 정의하는 것'과 '변수를 선언
하는 것'을 분리해 주므로 좋은 습관이다. 아래의 예를 참고하라.

struct _PEOPLE {

    int age;

    char name[10];

    long phone;

};//struct _PEOPLE형을 정의한다. 보기 좋게 필드를 각각의 줄에 명시한다.

...

struct _PEOPLE i■;//struct _PEOPLE형의 변수 i를 선언한다.

             C++에서는 _PEOPLE i; 처럼 변수를 선언한다.

  구조체 멤버 참조 연산자: . 과 ->■

             . 과 ->은 각각 점 연산자(dot operator), 화살표 연산자(arrow operator)
          라 읽는다.

  구조체의 필드를 접근(access)하기 위해 구조체 멤버■ 접근 연산자

             C에서 필드는 C++에서 멤버로 불린다.

(member access operator) . 과 ->를 사용한다. 아래의 예를 참고하자.

struct _PEOPLE {

    int age;

    char name[10];

    long phone;

};//struct _PEOPLE

struct _PEOPLE i;

  위의 예에서 struct _PEOPLE형의 구조체 i의 멤버 age를 접근하기 위해

i.age

라고 사용할 수 있다. 즉, 멤버를 접근하기 위해서 구조체-변수이름.필드-이
름형태의 문법을 사용한다. 아래의 예를 참고하라.

#include <stdio.h>

#include <string.h>

struct _PEOPLE {

    int age;

    char name[10];

    long phone;

};//struct _PEOPLE

void main() {

    struct _PEOPLE i;

    i.age=31;//이런 어느새 30이 넘어 버렸다.

    strcpy(i.name,"seojt");■

             아직 i.name="seojt"; 라고 사용하는 독자는 없다고 본다. 이 부분이 기억
          나지 않는 독자는 앞 장들(포인터,배열)을 참고하라.

    i.phone=9408690;

    printf("%d,%s,%ld\n",i.age,i.name,i.phone);

}//main()

출력 결과는

31,seojt,9408690

이다.

구조체 포인터가 사용된 경우

struct _PEOPLE* i;

처럼, 구조체 포인터가 선언된 경우에는 어떻게 멤버를 참조할까?

i.age=31;

은 명백한 잘못이다. i는 구조체가 아니라, 구조체를 가리키는 포인터이다. 즉
*i가 구조체이지, i는 4바이트의 메모리를 차지하는 포인터인 것이다. 그러므
로,

(*i).age=31;

처럼 사용해야 한다. 연산자의 우선순위 때문에 괄호를 반드시 명시해야 한
다. 하지만, i가 가리키는 곳이 아직 메모리 할당되지 않았으므로, 먼저 메모
리를 할당해야 한다.

    i=(struct _PEOPLE*)malloc(sizeof(struct _PEOPLE));

    (*i).age=31;

             구조체의 메모리 할당: i는 포인터이므로, 4바이트 할당되었다. malloc()에
          의해 16바이트(2+10+4)의 메모리가 할당된다. 그리고 이곳을 i가 가리키게
          된다.

  물론 프로그램의 적당한 곳에서 malloc()으로 할당한 메모리를 free(i)를 사
용하여 해제해야 할 것이다.

   malloc()의 리턴 값을 왜 (struct _PEOPLE*)로 형변환(type
conversion)해야 하죠?

   int* i와 char* i는 다르다. i가 같은 [1000]을 가지더라도, 덧셈 혹은
뺄셈에 의해 의미가 달라진다. int* i 인 경우 i+1은 [1002]이다. 이것은 i+1이
i에서 1만큼 떨어진 정수(interger)를 의미하므로, 컴파일러가 그렇게 해석하는
것이다. char* i 인 경우 i+1은 [1001]이다. 이것은 i에서 1만큼 떨어진 문자
(character)를 의미한다. 비록 포인터에 가감 연산이 취해지지 않더라도 컴파
일러는 이러한 불법 형 변환에 대해 경고한다. 만약 독자들의 프로그램에서
포인터에 대해 가감 연산이 필요 없다면, 단순히 이런 경고를 무시해도 좋다.
하지만, 정확한 형 변환은 좋은 프로그램 습관이다. 위의 예에서 i+1은 i값을
16증가시켜야 할 것이다. 그러므로, (struct _PEOPLE*)의 형 변환은 반드시
필요하다.

  구조체 포인터가 사용된 경우의 같은 예는 아래와 같다.

#include <stdio.h>

#include <string.h>

#include <alloc.h>

struct _PEOPLE {

    int age;

    char name[10];

    long phone;

};//struct _PEOPLE

void main() {

    struct _PEOPLE* i;

    i=(struct _PEOPLE*)malloc(sizeof(struct _PEOPLE));

    (*i).age=31;

    strcpy((*i).name,"seojt");

    (*i).phone=9408690L;

    printf("%d,%s,%ld\n",(*i).age,(*i).name,(*i).phone);

    free(i);

}//main()

출력 결과는

31,seojt,9408690

이다.

  구조체를 포인터로 사용할 일이 많을까? 그냥 사용할 일이 많을까? 구조체
는 포인터로 사용할 일이 대부분이며, 그냥 사용할 일은 거의 없다. 그렇다면,
i의 멤버 age를 참조하기 위해

(*i).age

라고 타이핑치는 일은 여간 번거로운 일이 아니다■. 그래서 특별하게 구조체

             또 다른 번거로움에 대해서는 '표준화유감'을 참조하라.

포인터에 대해서는 위의 예를

i->age

라고 사용하는 것이 가능하다. 즉 . 은 구조체의 멤버를 참조하기 위해서 사용
하며, ->는 구조체 포인터인 경우에 멤버를 참조하기 위해서 사용한다. 화살
표 연산자를 이용하여 수정된 소스는 아래와 같다.

#include <stdio.h>

#include <string.h>

#include <alloc.h>

struct _PEOPLE {

    int age;

    char name[10];

    long phone;

};//struct _PEOPLE

void main() {

    struct _PEOPLE* i;

    i=(struct _PEOPLE*)malloc(sizeof(struct _PEOPLE));

    i->age=31;

    strcpy(i->name,"seojt");

    i->phone=9408690L;

    printf("%d,%s,%ld\n",i->age,i->name,i->phone);

    free(i);

}//main()

  한 가지 의문이 생긴다. 실제로 컴파일러가 구조체의 멤버 참조를 어떻게
해석하는 것일까? 이것은 컴파일러 구현자의 입장에서 소스를 바라보는 것이
므로, 우리와는 조금 무관하다. 하지만, C++의 this를 이해하기 위해서, 이러한
내부 구조를 살펴보는 것은 많은 도움이 된다.

구조체의 필드를 바라보는 컴파일러의 입장: 상대 주
소(offset address)

i.age

는 컴파일러에 의해

[i의 시작주소]+0

으로 해석된다. 이것은 age가 구조체의 첫 번째 멤버이기 때문에 그렇다.

i.name

은 age 필드가 차지하는 메모리 공간을 건너뛰고 위치하고 있을 것이다. age
가 차지하는 메모리 공간은 2바이트 이므로,

[i의 시작주소]+2

로 해석된다. 컴파일러는 컴파일 시간에 필드의 형(type)을 정확하게 알고 있
으므로, 이러한 상대 주소의 계산이 가능하다.

i.phone

은 물론

[i의 시작주소]+12

로 해석될 것이다. 그렇다면, i의 시작 주소는 어떻게 결정되는가? 만약 구조
체가 전역 변수라면, 실행 파일이 메모리에 로드될 때 주소가 결정된다. 구조
체가 지역 변수라서 스택에 할당된다면, [i의 시작주소] 역시 스택 포인터를
참조하는, 상대 주소로 표현된다.

    {

    int k;

    struct _PEOPLE i;

    ...

    }

의 경우 [i의 시작주소]는 [stack-pointer+2]로 해석될 것이다. 왜냐하면, 이미
정수 k가 메모리를 2바이트 차지하고 있기 때문이다(스택에 관한 사항은
'scanf()에 왜 &가 필요한가?'를 참조하라). 물론 k의 주소는
[stack-pointer+0]으로 해석될 것이다.

  아래의 예를 보자.

#include <stdio.h>

#include <string.h>

#include <alloc.h>

struct _PEOPLE {

    int age;

    char name[10];

    long phone;

};//struct _PEOPLE

void main() {

    struct _PEOPLE* i;

    i=(struct _PEOPLE*)malloc(sizeof(struct _PEOPLE));

    (*i).age=31;

    strcpy((*i).name,"seojt");

    (*i).phone=9408690L;

    char* cp=(char*)i;//i의 주소를 복사한다.

    printf("%c\n",*(cp+2));//구조체에서 3번째 문자를 가리킨다.

    free(i);

}//main()

출력 결과는

s

이다. 이것은 우리가 직접 상대 주소를 계산한 것이다(이해가 잘 되지 않는
독자는 위의 메모리 구조를 참조해서 직접 메모리 상태를 그려보라).

   구조체의 초기화

  구조체는 배열 형식으로 초기화할 수 있다. 필드가 여러 개이므로, 블락으로
묶어 초기화해야 한다. 예를 들어 struct _PEOPLE 형의 p를 초기화하기 위해

struct _PEOPLE p={31,"seojt",9408690L};

와 같은 문장을 사용한다. 이것은 p.age, p.name과 p.phone을 각각 주어진 값
으로 초기화한다.

  C++의 새로운 연산자 new를 사용하여 구조체를 동적으로 할당할 때는,
new 다음의 괄호에 멤버의 초기값을 차례대로 명시한다. 아래의 예를 참고하
라.

struct S {

    int i;

    char c;

};

S■* s=new(5,'a');

             C++에서 struct S가 정의되었을 때 S는 새로운 데이터 형(type)이다. 그러
          므로, struct S* s; 가 아닌, S* s; 의 사용이 가능하다. 물론 이렇게 컴파일
          하기 위해서는 소스의 확장자가 CPP라야 한다.

  위의 예에서, 동적으로 메모리를 할당한 직후, s->i와 s->c를 각각 5와 'a'
로 초기화한다. C++의 생성자(constructor)에 대해 알고 있는 독자는 구조체
S의 생성자가, 멤버를 초기화하도록 만들어져 있다고 생각하면 된다.

   구조체의 전달(passing), 리턴(return)

  구조체를 함수의 파라미터로 전달할 때는 어떻게 할까? 이것은 단순하고
쉬운 작업이다(하지만, 어렵다). 아래의 예에서 struct _PEOPLE을 파라미터로
전달받고, struct _PEOPLE을 리턴하는 함수 IncAge()를 볼 수 있다.
IncAge()는 struct _PEOPLE의 필드 age를 1증가 시킨다.

#include <stdio.h>

#include <string.h>

#include <alloc.h>

struct _PEOPLE {

    int age;

    char name[10];

    long phone;

};//struct _PEOPLE

struct _PEOPLE IncAge(struct _PEOPLE p) {

    p.age++;

    return p;

}

void main() {

    struct _PEOPLE p={31,"seojt",9408690L};

    IncAge(p);

    printf("%d\n",p.age);

}//main()

예측하다시피 프로그램의 실행 결과는

31

이다. 우리는 이 문제를 'scanf()에 왜 &가 필요한가?'에서 다루었다. 프로
그램의 스택 동작(stack operation) 때문에 age는 32로 증가하지 않는다. 프로
그램을 고치는 방법은 2가지가 있다. 아래와 같이 소스를 수정해 보자.

...

void main() {

    struct _PEOPLE p={31,"seojt",9408690L};

    p=IncAge(p);

    printf("%d\n",p.age);

}//main()

  이제 출력 결과는

32

이다. 문제가 해결되었는가? 해결된 것은 분명하지만, 엄청나게 증가된 메모
리 복사(memory copy)의 오버헤드를 감수해야 한다. 자, 스택 동작에 의한
메모리의 구조 자세히 그려보자. 이 그림의 내용을 완전히 이해하는 것은 필
요하다. 독자들은 이 그림과 유사한 그림을 '복사 생성자'에서 다시 보게 될
것이다.

             main()에 struct _PEOPLE의 변수 p의 할당: struct _PEOPLE
          p={31,"seojt",9408690L}; 에 의해 스택에 p가 할당된다. name과 phone의 상
          대 주소가 +2, +12로 결정되었음을 주목하라.

  main()에 구조체 p가 할당되었다. p의 주소가 [5000]이라면, 멤버 name과
phone의 상대 주소는 각각 [5002]와 [5012]일 것이다.

             main()의 p가 IncAge()를 위해 스택에 복사된다: IncAge()의 호출은 main()의
          p를 값에 의한 전달(call by value)로 스택에 복사한다. IncAge()에서
          p.age++; 는 복사된(main()의 p가 아닌) 구조체의 멤버 age를 갱신한다.

  IncAge(p)는 main()의 구조체 p를 스택에 복사하여 함수 IncAge()에 전달
한다. p.age++는 main()의 p가 아닌 IncAge()의 p를 조작한다.

             return p; 의 실행: return p; 문장은 구조체 리턴을 위해 임시 구조체를 메
          모리에 할당한다. 이런 구조체가 복사되는 오버헤드를 생각해 보았는가!

  함수가 구조체를 리턴하는 사실에 주목하라. 함수가 객체(object)나 구조체
를 리턴하는 경우, 값을 복사하기 위해 임시 메모리에 복사된다. 즉, return p;
가 실행되기전, IncAge()의 구조체 p의 내용이 임시 메모리에 복사된다. 이러
한 복사 동작을 비트 단위 복사(bitwise copy)라 한다.

             IncAge()가 리턴했을 때의 메모리 상태: IncAge()가 종료한 후에도 임시 메
          모리에 할당된 임시 구조체(temporary structure)는 메모리에 남아 있다.

  이제 IncAge()를 리턴한 상태지만, 임시 구조체는 여전히 메모리에 남아 있
다.

             대입문에 의해 임시 구조체가 main()의 p에 복사된다: p=IncAge(p); 에 사
          용된 대입문은 IncAge()가 생성한 임시 객체를 main()의 p에 1비트도 틀림
          없이(bitwise) 복사한다. 이런! 구조체가 복사되는 오버헤드를 또 생각해 보
          았는가!

  main()에서 IncAge()가 리턴한 구조체 p가(실제로는 잠시 복사된 임시 구조
체) main()의 p로 비트 단위 복사된다.

             이제 main()의 p가 변경되었다: main()의 p.age는 32로 갱신된 것이 확실하
          지만, 구조체가 2번이나 복사되는 오버헤드 때문에 수행능력(performance)
          가 떨어진 것은 확실하다.

  이제 main()의 p가 갱신되었다. 하지만, 구조체의 멤버를 갱신하기 위해 2번
이나 구조체를 복사한 사실에 주목하라. 이것은 오버헤드이므로, 줄이는 것이
바람직하다. 2번째 해결방법은 무엇인가? 이것은 앞 장에서의 해결 방법과 같
은 것이다.

  구조체(C++의 객체)는 주소만을 전달하는 것이 바람직하다. 아래의 수정된
예를 참조하라.

#include <stdio.h>

#include <string.h>

struct _PEOPLE {

    int age;

    char name[10];

    long phone;

};//struct _PEOPLE

void IncAge(struct _PEOPLE* p) {

    p->age++;

}

void main() {

    struct _PEOPLE p={31,"seojt",9408690L};

    IncAge(&p);

    printf("%d\n",p.age);

}//main()

  출력 결과는

32

이다. 구조체의 주소를 나타내기 위해

&p

를 사용하였다. 이것은 그림에서 보듯이 [5000]을 의미한다. IncAge()의 선언
또한 포인터를 위해 적절히 변경되었다.

  구조체를 값으로 전달하는 것은 오버헤드가 크므로 어떤 컴파일러는 이러한
경우를 위해 경고를 출력하도록 설정될 수 있다.

             Borland C++ 4.5의 프로젝트 옵션 대화상자: 구조체가 값으로 전달되는 경
          우 경고를 출력하도록 설정할 수 있다.

  구조체를 함수의 파라미터로 전달하는 경우의 조언은 다음과 같다.

"구조체를 전달하지 말라, 항상 구조체의 주소를 전달하라. 만약 C++를
사용한다면, 구조체의 참조를 전달하라."

  구조체를 리턴하는 함수를 만들어야 한다면, 조언은 다음과 같다.

"구조체를 리턴하는 함수를 만들지 말라. 해야 한다면, 구조체의
레퍼런스(reference)를 리턴하라."

 레퍼런스에 관한 사항은 '참조(reference)'에서 다룬다. 물론 참조는 C++의
주제이므로, 확장자가 .C이면 제대로 컴파일되지 않는다.

   비트 필드 구조체(bit-field structure)

  구조체의 필드가 사용자에 의해 정의된 임의 크기의 비트로 설정될 수 있
다. 이러한 구조체를 비트 필드 구조체(bit-field structure)라 한다. 즉, 비
트 필드 구조체는 기본형(basic type)이 아닌 임의 크기의 비트를 필드로 가
진다. 비트 필드를 선언하기 위해서 다음과 같은 문법을 사용한다.

type-specifier [bitfield-id] : width;

  위의 문법에서 type-specifier는 char, unsigned char, int와 unsigned int만
이 될 수 있다. 콜론(:) 다음에 비트의 크기를 명시한다. bitfield-id는 비트 필
드 변수의 이름을 적는다. 공간만 확보할 목적이라면, id는 생략해도 좋다. 아
래의 예를 참고하라.

    struct SBitfield {

        unsigned i : 1;

        unsigned j : 1;

        unsigned k : 2;

    };

  위의 예는 필드로 i,j와 k를 가지는 구조체 struct SBitfield를 정의한 것이
다. i,j와 k를 위해 메모리는 각각 1비트,1비트와 2비트가 할당된다.

  실제 비트 필드 구조체의 메모리는 어떻게 할당되는 것일까? 이것은 컴파
일러 환경 설정에 따라 다르지만, 일반적으로 바이트 정렬(byte align)된다.
위의 예에서는 모두 4비트가 필요하므로, 메모리 1바이트가 할당될 것이다. 어
떤 컴파일러는 워드 정렬(word alignment)을 지원한다■. 워드 정렬로 설정

             16비트 시절 워드 정렬은 바이트 정렬 보다 조금 효율적인 면이 있었던
          것 같다. 예를 들면, 지역 변수로 char c; 를 선언해도, 실제 c는 메모리에
          2바이트 할당되었다. 물론 컴파일러는 이것을 속여 1바이트 할당된 것처럼
          보여주지만, 가변 인자 프로그램을 해보면, 스택에 할당되는 변수는 2바이
          트 단위라는 것을 확실히 알 수 있다. 이것은 IBM의 스택이 2바이트 단위
          로 워드 정렬을 했기 때문인데, 구현이 불가능한 것은 아니었지만, 효율을
          고려한 컴파일러 설계자의 선택이었을 것이다. 확실하지는 않지만, 같은
          이유가 구조체의 정렬에 적용되었을 것이다. 이것은 비트 필드 구조체에
          국한된 것이 아니다. 일반 구조체도 워드 정렬이 선택되면, 항상 2의 배수
          로 메모리가 할당된다.

된 컴파일러에서 위의 구조체는 2바이트가 할당되고, 그중 4비트만 사용되며,
나머지 12비트는 놀게 된다(unused). 그러므로, 비트 필드 구조체를 만들 때,
바이트 정렬이나 워드 정렬을 염두에 두고 설계하는 것이 바람직하다. 구조체
가 어떻게 정렬되는지 확인하기 위하여, 각자가 사용하는 컴파일러의 환경 설
정에서 옵션 메뉴를 참고하라.

  아래의 예는 비트 필드 구조체가 워드 정렬될 때의 적당한 사용을 보여 준
다.

#include <stdio.h>

#include <string.h>

struct SBitField {

    int         i : 2;

    unsigned    j : 5;

    int           : 4;

    int         k : 1;

    unsigned    m : 4;

};

void main() {

    struct SBitField s;

    s.i=-1;

    s.j=2;

    s.k=0;

    s.m=15;

    printf("%d,%d,%d,%d\n",s.i,s.j,s.k,s.m);

}//main()

  출력 결과는

-1,2,0,15

이다.

  위의 예에서 s는 다음과 같이 메모리 할당된다.

             비트 필드 구조체 SBitField의 메모리 구조: 전체의 크기가 계산되어 워드
          정렬된 후, 하위 비트(LSB)에서 상위 비트(MSB)로 비트 필드가 채워진다.
          이것은 인텔의 CPU가 역워드를 사용하기 때문만은 아니다. 일반적으로 데
          이터들은 낮은 바이트에서 높은 바이트로 채워 나가는 것이 관례이다.

  위의 예에서

s.i=6;

이라고 대입하면, 얼마가 출력될까? 비트 필드 i가 int로 선언되었다는 것에
주의하라. 6의 2진수 표현은

0110

이므로, 하위 2비트만이 s.i에 대입된다. 그러므로, s.i는 10이며 10은 2의 보수
표현으로 -2이므로■, -2가 출력될 것이다. 컴파일러가 이러한 사항에 대해서

             자세한 2의 보수 표현에 대해서는 '2진수, 2의 보수'를 참고하라.

경고(warning)을 출력하지 않으므로 주의해서 사용해야 한다.

   비트 필드와 일반 형을 썩어서 구조체를 만들 수 있는가?

   물론 만들 수 있다. 공용체와 결합하면, 필드의 일부를 꺼내오기 위해
서 비트 필드를 사용할 수도 있다. 기억나는가? 실수의 표현법을 2진수로 표
현하기 위해서, '데이터 형'에서 이러한 기교를 사용한 적이 있다. 그러므로,
아래의 문장은 문법적 에러가 아니다.

    struct SWow {

        unsigned i : 4;

        int j;

    };

비트 필드 클래스(?)(bit-field class)

  구조체가 비트 필드를 가질 수 있으므로, C++의 클래스가 비트 필드를 가
질 수 있는 것은 당연하다. 이것은 따로 언급하지는 않는다. 하지만, 독자들이
C++의 클래스를 익히고 나면, 스스로 비트 필드 클래스를 만들 수 있을 것이
다.

  왜 비트 필드 구조체를 사용하죠?

  독자들은 이러한 종류의 질문이 언제나 진부(old-fashioned)한 것이라는 것
을 기억해야 한다. 우리가 접하는 좋은 툴(tool)들은 모두 그 유용성이나 효용
성이 검증된 것이다. 독자들의 생각에 쓸 곳이 없다고 해서, 모두에게 쓸 곳이
없는 것이 아니라는 의미이다.

  그러므로, 이미 널리 알려진 툴의 사용에 대해서는

"왜 그런 도구들을 사용하죠? 필요가 없을 것 같은데..."

라고 묻기 보다는

"어디서 이런 도구들을 유용하게 사용하죠?"

라고 물어 보는 것이 바람직하다.

  DOS 시절에 이러한 구조체는 한글구현을 위해 빈번하게 사용했다. 지금은
운영체제가 한글 오토마타(automata)를 지원해 주지만, 비트 필드 구조체가
유용한 곳은 여전히 존재한다. 필자의 경우 컴퓨터 바둑을 구현할 때 판
(board)의 상태를 저장하기 위해, (메모리를 절약하기 위해) 비트 필드 구조체
배열을 사용했다.

  독자들은 또 요긴한 예를 아래에서 설명하는, 공용체와 함께 사용한 예에서
발견할 수 있을 것이다.

   진보된 주제: 구조체 필드, 자기 참조 구조
체와 구조체 배열

  구조체가 다른 구조체를 필드로 포함하는 것을 허락하는 것은 당연하다. 구
조체의 필드는 이미 정의된 모든 형이 필드로 선언될 수 있다.

  또한, 구조체 자체가 형을 정의하는 문장이므로, 구조체 안에서 구조체를 정
의하면서, 필드를 선언하는 것도 가능하다. 아래의 예를 참고하라.

struct SInner {

    int i;

    char j;

};

struct SOuter {

    struct SInner i;

    int j;

};

  위의 예에서는 이미 정의된 구조체 SInner를 struct SOuter에서 필드로 포
함하고 있다. struct SOuter의 변수는 (2+1)+2바이트의 메모리가 할당될 것이
다. 가시성(visibility)을 설명하기 위해 struct SInner와 struct SOuter에서
같은 i,j를 사용한 것에 주목하라. struct SInner와 struct SOuter는 다른 블락
범위를 가지므로, 필드 이름이 같은 것은 에러가 아니다.

  위의 예는 다음처럼 적어도 에러가 아니다.

struct SOuter {

    struct SInner {

        int i;

        char j;

    } i;

    int j;

};

  각각의 필드를 참조하기 위해 멤버 접근 연산자를 반복하여 사용한다. *.i.j
는 struct SOuter의 struct SInner의 j를 접근한다. *.j는 SOuter의 멤버를 접
근한다. 아래의 예를 참고하라.

#include <stdio.h>

#include <string.h>

struct SOuter {

    struct SInner {

        int i;

        char j;

    } i;

    int j;

};

void main() {

    struct SOuter i;

    i.i.i=1;

    i.i.j='A';

    i.j=2;

    printf("%d,%c,%d\n",i.i.i, i.i.j, i.j);

}//main()

  출력 결과는

1,A,2

이다. 위의 예에서 main()과 SOuter와 SInner의 블락 범위는 다르므로, 같은 i
를 사용하는 것은 타당하다. 즉, 구조체의 필드는 자신만의 구조체 이름 공간
(structure name space)을 형성한다.

불완전 선언(Incomplete Declaration)

  아래 그림과 같은 구조체를 생각해 보자.

             상호참조 구조체: 구조체가 다른 구조체의 시작 주소를 필드로 가진다. 그
          러므로, struct A를 정의할 때 struct B는 정의되어 있어야 한다. 반대로
          struct B를 정의할 때 struct A는 정의되어 있어야 한다. 이러한 모순을 어
          떻게 해결할 것인가?

  struct A는 struct B*를 필드로 가지면, struct B는 struct A*를 필드로 가
진다. 변수는 쓰기 전에 선언되어야하므로, struct A의 struct B* pb;를 필드
로 선언하기 위해서는 struct B 이미 정의되어 있어야 한다. struct B입장에서
는 struct A가 정의되어 있어야 한다. 이러한 상호 참조의 모순을 해결하기
위해서, 구조체나 클래스는 불완전 선언(incomplete declaration) 및 자기
참조(self referencing)를 허락한다. 아래의 예를 참고하자.

struct A;//불완전 선언

struct B {

    int i;

    struct A* pa;

};

struct A {

    char c;

    struct B* pb;//struct B는 이미 정의되어 있다.

};

  위의 코드 조각(program segment)에서 struct A; 를 불완전 선언이라 한다.
만약 이 문장이 없으면, struct B를 정의하는

struct A* pa;

는 정의안된 변수를 선언하려 했다는 컴파일 시간 에러를 유발한다. 그러므로,

struct A;

처럼 불완전 선언을 하여, 이 구조체가 아래에 정의될 것이라는 것을 컴파일
러에게 알려주는 것이다. 불완전 선언된 구조체는 물론 아래에서 완전하게 정
의되어야 한다.

  struct A를 정의할 때,

struct B* pb;

는 에러가 아니다. 왜냐하면, 이미 struct B가 struct A보다 위에 정의되어 있
기 때문이다. 아래의 예를 참고하라.

#include <stdio.h>

#include <string.h>

struct A;

struct B {

    int i;

    struct A* pa;

};

struct A {

    char c;

    struct B* pb;

};

void main() {

    struct A a;

    struct B b;

    a.c='A';

    a.pb=&b;

    b.i=65;

    b.pa=&a;

    printf("%c,%c\n",a.c, b.pa->c);

}//main()

  실행 결과는

A,A

이다.

  놀랍게도 구조체나 클래스는 자기 참조(self referencing)도 허락한다. 자
기 참조란 자신이 완전히 정의되기 전에 자신의 포인터를 필드로 가지는 기능
을 의미한다.

    struct A {

        char c;

        struct A* pa;

    };

  위의 예에서 struct A는 자신의 완전한 정의가 끝나기 전에

struct A*

를 포함하고 있다. 이것은 에러가 아니며, 연결 리스트 등을 구현하기 위해,
자주 사용하는 일반적인 방법이다.

  그렇다면, struct A의 변수는 메모리에 얼마나 할당되는 것일까? 위의 예에
서는 char c; 를 위해 1바이트, struct A* pa; 를 위해 4바이트, 모두 5바이트
가 할당된다.

  주의하라. struct A* pa; 는 일반적인 포인터 변수이기 때문에 위의 형식으
로 정의가 가능한 것이다. 그러므로, 아래의 문장은 문법적 에러이다.

    struct A {

        char c;

        struct A a;//struct A가 struct A를 필드로 포함할 수는 없다.

    };

  자기 참조 구조체의 완전한 아래의 예를 참조하라.

#include <stdio.h>

#include <string.h>

struct A {

    char c;

    struct A* pa;//struct A pa;처럼 사용하는 것은 불법이다.

};

void main() {

    struct A a1,a2;

    a1.c='A';

    a2.c='B';

    a1.pa=&a2;

    a2.pa=&a1;

    printf("%c,%c\n",a1.c,a2.pa->c);

}//main()

  출력 결과는

A,A

이다.

  자기 참조 구조체가 연결 리스트(linked list)를 구현하기 위해, 빈번하게 사
용함에도 불구하고, 이 장에서는 연결 리스트를 다루지 않는다. 이 주제는 '연
결 리스트'를 참조하라.

  이제 구조체에 대해서 살펴볼 마지막 사항은 구조체 배열(structure
array)에 관한 것이다. 이 주제는 C++의 객체 배열(object array)과도 연관
된 사항이므로, 완전하게 이해하기 바란다. 물론 어려운 것은 아니다.

  배열을 선언하는 문법을 상기해 보자. 그것은

형이름 배열이름[배열의크기];

와 같은 문법을 가졌다. 예를 들면,

int a[10];

은 정수를 10개 가지는, 그리고 대표이름 a로 참조하는 배열을 선언한 것이다.
물론, 배열의 대표 이름 a는 변수가 아니면, a[0],...,a[9]가 정수형 변수이다.

    struct A {

        int age;

        char name[10];

    };//struct A

  위와 같은 구조체 형이 정의된 경우 struct A가 형이므로, struct A형의 크
기 3인 배열을 선언하기 위해서는,

struct A a[3];

처럼 사용한다. 물론 a는 구조체 배열이 시작하는 곳의 시작 주소이며, a[0],
a[1]과 a[2]가 구조체이다. 그러므로, a[0]의 age를 접근하기 위해서는

a[0].age

라고 사용한다. 구조체 배열을 초기화하는 문법에 주의하며, 아래의 예를 참고
하라.

#include <stdio.h>

struct A {

    int age;

    char name[10];

};//struct A

void main() {

    struct A a[3]={{31,"seojt"},{31,"jangwg"},{31,"parkmg"}};

    int i;

    for (i=0;i<3;++i) {

        printf("%s, %d\n",a[i].name,a[i].age);

    }//for

}//main()

  출력 결과는

    seojt, 31

    jangwg, 31

    parkmg, 31

이다.

   공용체(union)

  공용체(union)는 그 이름이 의미하듯이, 필드들의 합집합(union) 만큼만 메
모리에 할당되는 것을 제외하고, 구조체와 문법과 사용법이 모두 같다. 아래의
예를 보자.

union A {

    char c;

    int i;

    long l;

};

  union A의 필드는 각각 1바이트, 2바이트 그리고 4바이트를 차지한다. 만약
이것이 구조체라면, 메모리는 7바이트가 할당되겠지만, 공용체는 자신의 필드
중 가장 큰 필드만큼만 메모리에 할당된다. 즉, union A는 4바이트의 메모리
가 할당된다.

  왜 공용체를 사용하는 것일까? 이유는 다음과 같다.

  ① 서로 배타적인 변수를 사용할 때

  ② 어떤 이유로 메모리에 존재하는 같은 변수의 다른 해석이 필요할 때

  위의 예에서 보듯이, 문자형(char), 정수형(integer) 그리고 긴 정수형(long
integer) 변수가 필요한데, 이들 변수가 동시에 사용될 일은 없다고 하자. 다
음과 같이 변수 선언을 할 수 있다.

    char c;

    int i;

    long l;

  물론 위의 선언이 틀린 것은 아니지만, 메모리의 효율 측면에서 아주 약간
비효율적인 것은 사실이다. 즉, c, i와 l이 동시에 사용될 일은 없음에도 불구
하고, 모두 메모리를 차지하고 있는 것이다. 그러므로, 가장 긴 l(4바이트)만큼
만 메모리를 확보하고, c와 i는 이 4바이트 영역을 공유하도록 하는 것이다.

  서로 배타적인 변수들을 공용체 선언으로 둘러싸면, 이 문제를 해결할 수
있다. 즉, 공용체를 정의한 다음,

union A a;

처럼 공용체 변수를 선언하면, a를 위해서 4바이트의 메모리가 할당된다. 그리
고, 문자형을 할당하여 쓸려면, a.c를 긴 정수형을 할당하여 쓸려면, a.l을 사용
하면 되는 것이다.

a.l=0x12345678L;

처럼 대입했을 때 실제 공용체 메모리는 어떻게 할당되는 것일까? 아래의 그
림을 보자.

             공용체의 메모리 할당: 각 필드가 메모리를 공유하여 할당된다. i80x 계열
          의 CPU는 역워드를 사용하므로, 이 점을 주의하라.

  위의 그림에서 보듯이 필드 l을 초기화했지만, i와 c의 값도 영향을 받는다
는 것을 주목하라. 또한 역워드 형식이라, 0x12345678은 거꾸로 저장되어 있
다. 즉, l이 [1000]에서 [1003]에 할당되었다면, 12는 [1003]번지에 들어 있다.

  위의 예에서 비록 a.l을 초기화했지만, a.i를 찍으면, 얼마가 찍힐까? 물론

5678

이 인쇄될 것이다. 역워드 형식이므로, 메모리있는 상위바이트 56을 정수 i의
상위바이트로 취급한다는 것을 주의하라. 아래의 예를 보자.

#include <stdio.h>

union A {

    char c;

    int i;

    long l;

};

void main() {

    union A a;

    a.l=0x12345678L;

    printf("%x,%x\n",a.c,a.i);

}//main()

  출력 결과는

78,5678

이다.

  위의 예에서 처럼, 공용체를 이용하면, 필드를 다른 방식으로 이용하는 코드
를 쉽게 작성할 수 있다. 즉, 사용자가 a.l을 초기화했음에도 불구하고, a.l의
상위 2바이트, 상위 1바이트를 읽는 코드를 작성한 것이 되는 것이다. 이것이
공용체를 사용하는 2번째 목적이다.

  우리가 이전에 살펴보았던, 부동 소수의 2진수 표현을 인쇄하는 프로그램은
이러한 공용체 사용의 좋은 예이다.

  공용체를 사용하여 부동 소수의 내부 표현을 2진수로 출력하는 소스 코드를
다시 리스트한다. 소스의 설명은 설명은 '데이터 형'을 참고하라.

#include <iostream.h>

struct BITFIELD {//이것은 구조체 중에서도 비트 필드 구조체이다.

    unsigned m0:4;//아주 특별해 보이는 이 선언은 m0가 4비트를 차지하는

                  //것을 의미한다.

    unsigned m1:4;

    unsigned m2:4;

    unsigned m3:4;

    unsigned m4:4;

    unsigned m5:4;

    unsigned m6:4;

    unsigned m7:4;

};//struct BITFIELD

union FLOAT {//공용체는 필드(field)를 공유한다. f와 b는 같은 메모리를 공

             //유하며, f의 크기가 32비트, b의 크기 역시 32비트이므로 실

             //제 메모리는 64비트가 아닌 32비트가 할당된다.

    float f;

    BITFIELD b;

};//union FLOAT

void main() {

    FLOAT f;

    f.f=-14.24;

    //cout은 ostream 클래스의 전역 객체이다.

    cout << f.f << endl << hex;//endl과 hex 조작자(manupulator)는 출력

                               //스트림의 형식(format)을 조작한다. 이것

                               //은 '함수 포인터'에서 자세히 다룬다.

    cout << f.b.m0 << f.b.m1 << f.b.m2 << f.b.m3

        << f.b.m4 << f.b.m5 << f.b.m6 << f.b.m7 << endl;

    //output: c163d70a■

}

무명 공용체(anonymous union)

    union A a;

    a.l=0x12345678L;

을 다시 고려해 보자. 사용자의 목적은 l과 c와 i를 사용할 목적이었지만, 더
미(dummy)로 a를 사용한 것이다. a를 사용하지 않고 c,i와 l을 바로 사용할
수는 없을까? C++ 표준은 이러한 공용체를 지원하는데, 이것을 무명 공용체
(anonymous union)라 한다. 무명 공용체는 태그 이름을 가질 수 없으며, 변
수 이름도 가질 수 없다. 아래의 예를 참고하라.

#include <stdio.h>

void main() {

    union {//무명 공용체

        char c;

        int i;

        long l;

    };

    l=0x12345678L;//이제 l 및 i,c를 그냥 사용할 수 있다.

    printf("%x,%x\n",c,i);

}//main()

  출력 결과는

78,5678

이다.

   연결 리스트(linked list)

  자기 참조 구조체를 이용하여, 링크드 리스트를 만드는 것은 '링크드 리스
트'에서 다룬다.

  독자들은 이 밖에 구조체에 관한 진보된 주제들을 'C++의 구조체'에서 참
조할 수 있을 것이다.

  실습문제

1. 구조체가 값으로 전달될 때의 그림을 참조하여, 구조체 포인터가 전달될 때
의 메모리 상태를 차례대로 그려보라.

2.


18. 파일

18. 파일(file)

  대부분의 책에서 파일을 다룰 때, 파일 구조체 포인터(FILE* fp)를 사용
하는 fopen() 계열의 함수에 대해서 다룬다. 하지만, 이 장에서는 이러한 표준
함수(이러한 종류의 함수는 ISO표준이므로 모든 컴파일러에서 지원한다)를
다루지 않을 것이다(이러한 함수들은 다른 책들을 참조하라). 대신, 핸들
(handle)을 사용하는 핸들계 파일 조작함수들만을 다룰 것이다■. 핸들을

             이 함수들은 표준이 아니지만 모든 컴파일러에서 지원하고 있다.

사용하는 파일 조작 함수들은 훨씬 빠르며, 유동적이다.

  이 책에서는 또한, C++표준 라이브러리인 iostream에서 지원하는 파일 조작
클래스(fstream)에 대해서도 언급하지 않을 것이다. 이것은 C++ 표준 라이브
러리이기는 하지만, 널리 사용하지는 않는다.

  핸들(handle)이란?

  핸들(handle)이란 운영체제(operating system)가 특정한 정보를 유지하기
위해서, 메모리에 유지하는 정보 블락(information block)에 붙여진 고유 번호
(unique number)를 말한다. 예를 들면, 프로세스 컨트롤 블락(PCBs:
Process Control Blocks)은 실행 중인 각각의 프로세스■의 정보를 관리한

             디스크의 프로그램인 실행 중인 상태를 프로세스(process) 혹은 태스크
          (task)라 한다. CPU를 뜻하는 프로세서(processor)와 구분하라.

다. 프로세스가 여러 개라면, 이러한 블락은 메모리에 여러 개 유지되고 있을
것이다. 이러한 각각의 블락은 고유한 번호가 할당되는데 이 번호를 프로세스
핸들(process handle)이라 한다. 사용자가 프로세스에 관한 정보를 알려면,
운영체제의 함수를 호출할 때, 첫 번째 파라미터로 프로세스 핸들을 넘겨 주
어야 할 것이다.

             Win95의 프로세스 상태: Ctrl+Alt+Del을 누르면, 프로세스 컨트롤 블락에서
          정보를 읽어 태스크 원도우(task window)를 보여 준다.

  독자들은 왜 이 번호를 핸들(handle)이라 하는지 이제 이해할 것이다. 핸들
번호만 알면, (자동차 핸들로 자동차를 조작하듯이) 이 핸들이 가리키는 정보
를 마음대로 조작(handle)할 수 있는 것이다.

  사용자가 파일에 관해서 입출력 작업을 하기 위해서는 먼저 파일 핸들(file
handle)을 얻어야 한다. 파일 핸들은 파일 제어 블락(FCBs: File Control
Blocks)에 붙여진 고유 번호이다. 파일 제어 블락은 디스크에 존재하는 파일
에 입출력 작업을 하기 위해서 다양한 정보를 유지하고 있는 블락이다■.

             이러한 정보 블락은 운영 체제에 의해 구조체로 표현된다. 또한 이러한
          구조체들은 링크드 리스트로 (아마도) 유지된다.

  파일 제어 블락을 메모리에 할당한 다음, 디스크 파일에 관한 정보로 이 구
조체 블락의 필드를 초기화하는 것을 파일을 연다(open a file)고 한다. 예를
들면,

open("파일이름","파일모드");

함수는 파일을 연다. 물론 이 함수의 파라미터로 열고자 하는 파일 이름, 파일
입출력 모드(file I/O mode)를 설정해 주어야 한다.

  파일에 관한 함수는 크게 다음과 같이 구분할 수 있다. 함수의 이름은 컴파
일러에 따라 차이가 날 수 있지만, 비슷하다.

① 파일 열기(open),닫기(close) 함수

② 파일 입출력(read/write) 함수

③ 파일 포인터(file pointer)■ 조작 함수

             파일의 현재 읽고 쓰는 위치를 가리키는 정수형 변수를 파일 포인터(file
          pointer)라고 한다. 파일 포인터는 밑에서 자세히 다룬다.

④ 기타(etc.) 함수

① 파일 열기(open),닫기(close)

  파일 제어 블락을 할당하고, 핸들을 얻는 것을 '파일을 연다'고 한다. 메모리
에 할당된 파일 제어블락을 해제하는 것을 '파일을 닫는다'고 한다. open(),
close()가 각각 이 일을 담당한다. open()은 파일 핸들을 리턴하고, close()는
파일 핸들을 파라미터로 받아야 할 것이다.

② 파일 입출력(read/write)

  파일 제어 블락의 핸들을 이용해서 디스크 파일로부터 읽고(read()), 쓰는
(write())일을 하는 함수들이다. 이러한 함수들의 첫 번째 파라미터는 파일 핸
들이어야 할 것이다.

③ 파일 포인터(file pointer) 조작

  파일 포인터의 위치를 설정하는 함수들이다. 파일 포인터를 처음으로 혹은
끝으로 옮기거나, 특정한 위치로 옮긴다. lseek(), seek()등의 함수가 있다.

  실제 입출력 함수들은 파일 포인터를 기준으로 작업한다. 예를 들면, read()
함수는 현재 파일 포인터가 가리키는 곳에서 읽기를 시작한다.

④ 기타(etc.)

  파일을 끝인지를 검사하는 eof(), 파일의 길이를 구하는 getlength(), 파일의
크기를 변경시키는 chgsize(), 파일을 제거하는 remove(), 파일 모드를 설정하
는 setmode()와 파일을 락(lock)시키는 lock()등이 있다.

  파일의 사용

  이제 실제로 프로그램에서 파일을 어떻게 사용해야 하는지 살펴보자. 제일
먼저 해야 할 일은 open()을 사용하여 파일 제어 블락(FCB)을 할당받고 핸들
을 얻는 것이다. 예를 들면, C:\Windows\Win.ini 파일을 이진 파일(binary
file)■로 읽고 쓰기 위해서 열려면 다음 문장을 사용한다.

             이진 파일과 텍스트 파일(text file)에 관한 주제는 이 장의 뒤에서 설명한
          다.

    int handle;

    handle=open("c:\\windows\\win.ini",O_RDWR|O_BINARY);

  open()의 첫 번째 파라미터는 파일명이며, 두 번째 파라미터는 파일 모드를
비트 플래그로 설정한다. 이러한 함수들과 O_RDWR(Open ReaD and WRite)
등의 상수를 사용하기 위해서는

    #include <io.h>

    #include <fcntl.h>

    #include <sys/stat.h>

처럼 필요한 헤더를 포함시켜야 한다. open()의 리턴 값을 받기 위해 단지 정
수(integer)만을 사용했다는 것에 주목하라. 다른 것은 필요치 않다.

             open()의 동작: 디스크에 존재하는 파일의 정보를 새로 할당한 FCB에 채
          운다. 그리고 핸들(이 경우 8)을 리턴한다. 디스크에 파일이 없거나, FCB를
          할당할 공간이 없거나 등의 에러인 경우, open()은 -1(0xFFFF)을 리턴한다.
          이렇게 할당한 FCB블락은 후에 close()를 사용하여 해제해야 한다.

  이제 파일에 관한 정보가 FCB에 채워졌고, 파일 핸들(예를 들면 8)을 알고
있으므로, 파일에 입/출력을 할 수 있다.

  예를 들면,

    unsigned char c;

    read(handle,&c,1);

은 파일에서 1바이트를 읽어 c에 저장한다. read()의 파라미터는 처음부터 각
각

파일 핸들, 파일에서 내용을 읽을 메모리 주소, 읽을 바이트 크기

를 명시한다. 위의 예에서 c에는 무엇일 읽힐까? win.ini파일의 첫 번째 바이
트가 읽힐 것이며, 대부분의 경우에 그것은 '['이다.

             read(), write()의 동작: read()와 write()등의 입출력 함수는 첫 번째 파라미터
          에서 명시된 파일 핸들(8)을 이용하여 실제 디스크의 파일에 입/출력을 한
          다. 파일 핸들이 가리키는 파일 제어 블락(FCB)에는 입/출력에 필요한 모
          든 정보들이 들어 있다.

필자의 경우 win.ini 파일의 앞 일부분은 다음과 같다.

[windows]

NullPort=None

ScreenSaveActive=1

ScreenSaveTimeOut=300

SkipMouseRedetect=0

device=HP DeskJet 890C Series,HPRDJC06,LPT1:

[Desktop]

Wallpaper=(없음)

...

   win.ini파일의 일부분

  파일에 관한 입출력 작업이 모두 끝났다면, 이제 FCB를 다시 운영체제에게
돌려주어야 한다. 그것은 close()를 사용하여 다음과 같이 첫 번째 파라미터로
파일 핸들을 명시한다.

    close(handle);

             close()의 동작: close()는 운영체제에 할당된 FCB를 다른 파일이 사용할
          수 있도록 메모리에서 해제한다. win.ini 파일에 관한 정보는 이제 참조할
          수 없다.

  위의 동작을 코딩한, 아래의 예를 참고하자.

#include <stdio.h>

#include <string.h>

#include <io.h>

#include <fcntl.h>

#include <sys/stat.h>

void main(){

    int handle;

    unsigned char c;

    handle=open("c:\\windows\\win.ini",O_RDWR|O_BINARY);

    if (handle==-1) return;

    read(handle,&c,1);

    printf("%c",c);

    close(handle);

}//main()

  출력 결과는

[

이다.

  위의 예에서

read(handle,&c,1);

에 의해 읽힌 문자는 win.ini 파일의 첫 번째 문자인

[

이다. read()를 2개 더 추가하여, main()의 소스를 다음과 같이 수정하면,

    ...

    if (handle==-1) return;

    read(handle,&c,1);

    printf("%c",c);

    read(handle,&c,1);

    printf("%c",c);

    read(handle,&c,1);

    printf("%c",c);

    close(handle);

}//main()

  출력 결과는

[wi

이다. 즉 두 번째 read() 호출은 w를 읽었고, 세 번째 read() 호출은 i를 읽었
다. 어떻게 이것이 가능한가?

  파일 포인터(file pointer)

  FCB에 유지되는 중요한 멤버 중 하나가 바로 파일 포인터(FP: File
Pointer)이다. 파일 포인터는 일반적으로 4바이트 정수로 유지되는데, 파일에
서 입/출력 작업이 일어나야 할, 파일의 처음에서 상대주소(relative address)
를 가리킨다.

  파일을 여는 순간 FP는 0으로 초기화된다. 이것은 입/출력이 파일의 처음에
서 일어나야 함을 가리킨다.

read(handle,&c,1)

함수는 자세하게 다음과 같이 동작한다.

"handle이 가리키는 FCB에서 FP를 읽어서, FP가 가리키는 곳에서
1바이트를 &c로 복사하고, FP를 1증가시킨다."

  즉 read()와 write()등의 함수는 자동으로 FP를 증가시킨다. 파일에 읽거나
쓰는 일없이 FP를 조작하려면, lseek()등의 파일 포인터 조작함수를 사용한다.

    char s[10];

    read(handle,s,10);

은 어떻게 동작할까?

"현재 FP가 가리키는 곳에서 10바이트를 s로 복사한다. FP는 10 증가한다."

  이제 세 개의 연속된 read()함수가 왜 [wi 를 읽었는지 이해할 것이다. 우리
는 최종적으로 win.ini파일을 모두 화면에 출력하는 프로그램을 만들 것이다.
하지만, 더 진행하기 전에 이진 파일과 텍스트 파일의 차이점을 알아보자.

   이진 파일 vs. 텍스트 파일

  독자들은 볼런드 함수 cprintf()와 표준 함수 printf()의 차이점을 알고 있는
가?

  옛날의 화면(console)은 컬러를 지원하지 않았다. 하지만, DOS를 기반으로
하는 IBM의 새로운(?) 컬러 모니터는 텍스트 모드에서 16컬러를 지원했다.
그러면, 색을 지원하도록 printf()를 수정해야 하는가? 그렇지 않다. printf()는
색을 지원하지 않는 표준함수이므로, 이 함수를 수정할 수는 없다.

  그래서 볼런드는 자사의 컴파일러에 색을 지원하면서, printf()와 똑 같이 동
작하는

cprintf(), cscanf()■

             이러한 함수들은 conio.h를 포함시켜야 사용할 수 있다.

등의 함수를 추가했다. 첫 자 c는 아마도 Color를 의미할 것이다. 독자들은 이
런 류의 함수가 DOS외에서는 동작하지 않는다는 것을 알고 있어야 한다.

  아래의 프로그램은 hello를 흰색(WHITE)으로, 다음 줄에 world를 밝은 초
록색(LIGHTGREEN)으로 출력하는 소스이다. 출력 결과에 주목하라. 조금은
당황된다.

#include <conio.h>

void main() {

    textcolor(WHITE);

    cprintf("hello\n");

    textcolor(LIGHTGREEN);

    cprintf("world\n");

}

  출력 결과는

hello

     world

이다. 왜 world는 2번째 줄의 처음에서 시작하지 않았을까? 이것이 이진 파일
과 텍스트 파일의 차이점을 구별하는 열쇠이다.

고전(oldest), 그러나 ASCII

  이제 세계 표준 7비트 코드인 ASCII(American Standard Code for
Information Interchange)를 다시 다룰 때가 되었다. 이 코드는 Win95의 가
상 키 코드(virtual key code) 때문에, 힘이 약해진 것은 사실이지만, 여전
히, 윈도우의 WM_CHAR등의 메시지에서는 ASCII 코드가 파라미터로 넘어
오고 있다■.

             Win32 아키텍쳐(architecture)에 대해서는 '가상함수' 및 'RTTI'를 참고하라.
          하지만, Windows의 자세한 사항은 이 책에서 다루지 않는다.

  ASCII는 7비트 코드이며, 표준이다. 이것은 27=128개의 서로 다른 문자를
표현할 수 있으므로, 키보드의 100여 개의 서로 다른 문자를 유일하게 표현할
수 있다■.

             PC는 8비트 단위의 기본 코드를 사용하므로(이것의 이유에 대해서는 앞
          장에서 설명한 바 있다), ASCII도 8비트로 확장되었다. 실제 이러한 확장
          아스키(Extended ASCII)는 IBM에서 정한 회사 코드였지만, IBM 호환 PC
          가 대중화되는 덕분에 확장 아스키 역시 모든 PC가 지원했었다. 하지만,
          표준은 아니므로, Windows에서 이러한 확장 아스키는 더 이상 지원하지
          않는다. 물론 Windows 자체가 유니코드(Unicode)를 사용하므로, 표준 아스
          키도 지원하지 않는다. 하지만, 유니 코드의 하위 7비트는 아스키와 호환
          된다. 유니코드를 직접 입력하는 방법은 없을까? 물론 있다. 지금 메모장
          (notepad.exe)를 실행해 보라. 그리고, Alt키를 누른 채로, 숫자 키 패드
          (numeric keypad: 화살표와 숫자가 같이 있는 키판)의 '0','6','5'를 차례대로
          누른 다음, Alt키를 뗀다. 메모장에는 'A'가 찍힌다. 이런 식으로 'B', 'a'등도
          입력할 수 있다. 물론 도스창에서는 아스키를 지원하므로, Alt+'6','5'처럼 눌
          러야 한다.

  이러한 아스키코드는 크게 출력 가능한 문자(printable character)와 제어
문자(control character)로 나눌 수 있다.

    0~31: 제어 문자

    32~126: 출력 가능 문자

    127: 제어 문자

  예를 들면, Enter는 13번이다. 이것은 화면에 뭔가를 출력하는 것이 아니라,
커서를 다음 줄로 보내거나, 명령을 입력하는 등의 제어를 담당한다. Escape
는 27번이다. 이것 역시 제어를 담당하며(도스 명령행(command prompt)에서
는 명령을 취소하는 일을 한다), 뭔가를 출력하지는 않는다.

  32에서 126까지의 출력 가능한 문자의 중요한 아스키코드는 다음과 같다.

    32: 공백

    48: '0'(문자 0)

    65: 'A'■

             소문자와 대문자의 차이가 32이다. 이것은 대문자와 소문자를 토글(toggle)
          시키는데 사용되는 값이다. 예를 들면, toupper()등의 매크로 함수는 32를
          빼거나 더한다.

    97: 'a'

  첫 번째 출력 가능한 문자가 공백(blank)인 것은 흥미롭다.

  우리가 살펴보지 않은 0에서 31사이의 제어문자에는 '데이터 전송의 끝', '데
이터 전송의 시작', '파일의 끝', '문자열의 끝' 등을 나타내는 각각의 제어 기
능이 예약되어 있다.

  10은 '커서를 다음 줄로'를 의미하는 'New Line' 문자이다. 13번은 '커서를
줄의 처음으로'를 의미하는 'Carriage Return' 문자이다. C++에서 10번과 13
번은 문자로 나타낼 수 없으므로, 각각 Escape 절차를 사용하여, '\n', '\r'로
나타낸다.

  키보드의 Enter를 치면, 2개의 문자가 발생한다. 그것은 차례대로 13번과 10
번 문자이다. 그러므로, 스트링 표현에서 줄을 바꾸는 표현은

printf("hello\n");

이 아니라,

printf("hello\r\n");

이 바른 것이다. 그런데 왜 printf()등의 함수에서는 \n이 줄을 바꾸는 역할을
하는 것일까? 그것은 바로 다른 운영 체제(예를 들면 UNIX)와의 차이점 때문
이다. UNIX에서는 줄을 끝을 나타내는 문자는 '\n'(10)이다. 하지만, 도스
(DOS)에서는 '\r\n'(13 10)이다. 하지만, printf()는 표준함수이므로 일관된 표
현을 가져야 한다. 그러므로, DOS 운영체제의 컴파일러는 printf()등의 표준
함수에 사용된 "\n"을 "\r\n"으로 자동으로 번역한다. 하지만, cprintf()등은 표
준 함수가 아니므로, 프로그래머가 명시적으로 "\r\n"을 붙여 주어야 하는 것
이다. 위의 예에서는 \n만 사용되었으므로, 커서가 다음 줄로 넘어 갔을 뿐
(new line) 커서가 줄의 처음으로 가지는(carriage return) 않았다.

  문제는 이것이 아니다. 그렇다면, 파일에 있는 0에서 31사이의 제어 문자를
어떻게 해석해야 하는가? 제어 문자로 해석해야 하는가? 데이터로 해석해야
하는가? 제어 문자로 해석하면, 이것은 텍스트 파일(text file)이다. 데이터로
해석하면, 이것은 이진 파일(binary file)이다.

  아스키코드의 범주에서 말하면, 32~126번 사이의 문자와, 제어 문자 중 줄을
바꾸는 특수문자 13,10과, 파일의 끝을 나타내는 특수문자 0xFF(-1: 이것은
stdio.h에 EOF로 정의(define)되어 있다)를 포함한 파일을 '텍스트 파일'이라
하며, 그 이외의 것을 '이진 파일'이라 한다. 물론 텍스트 파일을 이진 파일로
열어서 처리해도 상관은 없다.

  그러면 파일을 이진 파일로 여는 것과, 텍스트 파일로 여는 것에는 어떤 차
이가 있는가? 그것은 표준 함수(fscanf(), fprintf()등)가 동작하는 방식을 결정
한다. 파일을 텍스트 파일로 열었을 경우 문자열의 끝에 있는 13은 읽히지 않
는다. 이것은 UNIX와의 호환을 위해서 문자열의 끝에는 10이 있다고 가정하
기 때문이다. 하지만, 이진 파일로 열었을 경우 문자열의 끝에 있는 13,10이
모두 읽힌다. 아래의 그림을 참조하자.

    God loves

    YOU

란 이진 파일은 DOS와 UNIX에서 각각 아래 그림처럼 저장된다.

             DOS 텍스트 파일과 UNIX 텍스트 파일: DOS의 문자열의 끝에는 13,10이
          있지만, UNIX의 문자열의 끝에는 10이 있다. 하지만, fprintf(), fscanf()등의
          함수는 일관되게 동작해야 하므로, 도스의 fscanf()등의 함수는 텍스트 파일
          의 13을 단순하게 무시한다. 이러한 차이점은 미묘한 문제를 일으킬 수 있
          다.

  위의 텍스트 파일을 data.txt라 하자. 그러면, DOS와 UNIX의

    char s[80]

    FILE* fp=fopen("data.txt","r");

    fscanf(fp,"%s",s);

문장은 s에

God loves\n

을 저장한다. DOS의 경우는 13이 무시되었다는 것을 주목하라. 하지만,
data.txt를 이진 파일로 열었을 경우 아래의 소스는

    char s[80];

    int handle=open("data.txt",O_BINARY);

    read(handle,s,10);

은 DOS와 UNIX 모두의 경우 10바이트만 읽으므로, DOS 텍스트 파일인 경
우는

God loves\r

이 읽히고, UNIX에서 만든 텍스트 파일인 경우

God loves\n

이 읽힌다■.

             독자들은 fgetch()나 getc()등에서 이러 미묘한 문제 때문에 고생해 본적이
          있는가? 그래서 필자는 텍스트 파일이라도 절대로 텍스트 모드로 파일을
          열지는 않는다.

  텍스트 파일의 처리

  텍스트 파일을 2진 파일로 열어서 줄(line)을 읽고, 왼쪽과 오른쪽의 공백을
제거하는 프로그램을 구현해 보자. 아래의 예에서 ReadLine()은 한 줄을 읽는
다.

int ReadLine(int handle, char s[]){

    char ch=0;

    int i=0;

    if (eof(handle)) return -99;

    while (ch!=10 && !eof(handle)■){

             이진 파일에는 파일의 끝에 EOF 마크가 없다. 그렇다면, eof()함수는 어떻
          게 동작하는 것일까? eof()는 단순히 파일 포인터가 파일의 크기를 넘었는
          지 검사한다.

        read(handle,&ch,1);

        s[i++] = ch;

    }//while

    s[i]=0;//stuff EOS mark

    return i-2;//return length of the string

}//ReadLine()

  줄의 끝을 검사하기 위해 10을 검사했다는 사실에 주의하고(13은 10전에 있
으므로, 10을 검사해야한다. 이것은 UNIX와의 호환을 위한 DOS 설계자의 배
려이다), EOS 마크를 추가했다는 사실에 주목하라. 그리고, 스트링의 길이를
리턴하기 위해, 읽은 바이트에 -2(13,10 제외)를 했으며, 파일의 끝인 경우는
-99를 리턴한다.

  LTrim()과 RTrim()은 각각 왼쪽과 오른쪽의 공백을 제거한다. Trim()은 이
둘을 호출하여, 앞, 뒤의 공백을 모두 제거한다.

  아래의 소스는 win.ini 파일의 내용을 모두 출력한다.

#include <stdio.h>

#include <string.h>

#include <io.h>

#include <fcntl.h>

#include <sys/stat.h>

int ReadLine(int handle, char s[]){

    char ch=0;

    int i=0;

    if (eof(handle)) return -99;

    while (ch!=10 && !eof(handle)){

        read(handle,&ch,1);

        s[i++] = ch;

    }//while

    s[i]=0;//stuff EOS mark

    return i-2;//return length of the string

}//ReadLine()

int LTrim(char s[]){

    int i=0;

    char ch=s[i];

    while (ch==' '){

        ch=s[++i];

    }//while

    memmove(&s[0],&s[i],strlen(s)-i+1);

    return i;//return the number of removed blanks

}//LTrim()

int RTrim(char s[]){

    int i=strlen(s)-1;

    int count=0;

    char ch;

    ch=s[i];

    while (ch==' '){

        ch=s[--i];

        count++;

    }//while

    s[i+1]=0;

    return count;//return the number of removed blanks

}//RTrim()

char* Trim(char s[]){

    LTrim(s);

    RTrim(s);

    return s;

}//Trim()

void main(){

    int handle;

    handle=open("c:\\windows\\win.ini",O_RDWR);

    if (handle==-1)

        return;

    char s[300];

    while (ReadLine(handle,s)!= -99){

        printf(Trim(s));

    }//while

    close(handle);

}//main()

  출력 결과는

(win.ini 파일의 내용이 한 줄씩 모두 출력)

이다.

   이진 파일의 처리

  이제 좀 더 흥미 있는 프로그램을 만들어 보자. 우리가 만들 프로그램은
Windows의 도스창(command prompt)에서 사용하는 내부 명령어(internal
commands)■를 사용자가 편집할 수 있는 편집기이다. 도스창은

             이제는 아득한(?) 도스시절 사용자는 오로지 명령행을 통해서, 명령을 입
          력하고 프로그램을 실행했다. 이러한 명령은 때때로 GUI보다 강력하므로,
          아직도 도스창은 가끔식 사용된다. 빈번하게 사용하는 명령은 외부에 파일
          형태로 존재한 것이 아니라, command.com에 내장되어 있었다. 이러한 명
          령을 내부 명령(internal command)라 불렀다. 사용자가 입력한 명령은 대문
          자로 변환된 후, command.com에 내장된 내부 명령과 비교되었다.

C:\Windows\command.com■을 실행하는 것이다. command.com은 내부 명령

             실행 파일의 확장자가 왠 COM? Windows용 컴파일러에서 확장자가 COM
          인 실행 파일을 만드는 방법은 이제 없어 졌다. 16비트 도스 시절, 8비트
          실행 파일과의 호환을 위해, 부득이 선택했던, 옛날 실행 파일의 확장자는
          작다(COMpact)를 뜻하는 COM 이었다. 그리고, 그냥(?) 실행 파일은
          EXEcutable의 두음문자(initial)인 EXE 였다. 32비트 윈도우 98 운영체제에서
          실제 존재하는 실행 파일(DLL 및 OCX는 제외)의 종류는 4가지이다. 도스
          용 COM과 16비트 EXE, Win16에서 동작했던 EXE인 New Executable
          Format(이러한 EXE 파일은 도스에서 실행하려고 하면, 'This program
          needs Microsoft Windows'를 출력했었다), 그리고, 지금 Win32에서 동작하
          는 32비트 EXE가 그것이다. 윈도우 98에서는 이 모든 실행 파일을 실행할
          수 있다.

어 테이블(internal command table)을 가지고 있다. 우리가 이 테이블을 찾아
서 원하는 명령을 고치면, 내부 명령을 고칠 수 있다. DIR명령을 LST로 고치
는 것은 얼마나 흥미로운 일인가!

이진 에디터(binary editor)

  먼저 이진 에디터를 실행하여 command.com의 내부를 확인해 보는 작업이
필요하다. 독자들은 이진 에디터(일반적으로 16진 에디터(hexa editor)라고 부
른다)를 통신망에서 쉽게 구할 수 있다. 필자는 Microsoft Developer Studio
6.0을 주로 사용한다. 이 통합 에디터는 실행 파일의 리소스(resource)를 편집
할 수 있을 뿐만 아니라, 모든 종류(*.bmp, *.ico. *.cur, *.jpg, *.htm 등)의 파
일을 편집할 수 있는 막강한 기능을 제공한다.

  Visual C++ 6.0을 실행해서, File->Open을 선택한다. 그러면 아래와 같은
대화상자가 실행된다.

             Visual C++ 6.0의 File->Open: 각 종류의 파일을 원하는 모드로 열 수 있
          다. Open as에서 Binary를 선택하여 c:\windows\command.com을 연다.

  Open as 콤보 박스(combo box)에서 Binary를 선택한 후,
C:\Windows\command.com을 선택한다. 그러면 아래와 같이 이진 편집 창
(binary editor window)이 뜬다.

             command.com의 내부: 상대 주소 16820번지에서 내부 명령어 테이블을 발
          견할 수 있다. 0x14, 0x56, 0x98, 0x87로 시작하며, 0x00으로 끝난다는 것을
          확인할 수 있다. Win95의 경우는 다를 수 있으므로, 주의하라. 이 파일은
          한글 윈도우 98의 command.com이다.

내부 명령어 테이블의 구조

  위 그림에서 보듯이 Windows 98 command.com의 내부 명령어 테이블은
파스칼(Pascal) 언어의 문자열 규칙대로 표현되어 있다. 즉,

문자열의 길이 + 문자열

로 테이블이 구성되어 있다. 처음 몇 명령을 보면, DIR과 CALL의 길이는 각
각 3,4 이므로, 테이블에

3 D I R 4 C A L L ...

형식으로 명령이 들어 있다. 이제 이 테이블의 시작을 발견하여, 첫 번째 명령
인 DIR을 출력하는 프로그램을 만들어 보자. 우리는 이 프로그램을 조금씩
개선하여 나갈 것이다.

  아래의 프로그램은 command.com의 처음부터 한 문자씩 (출력 가능한 문자
만) 화면에 출력한다. 그리고, 내부 명령어 테이블을 찾은 후, 첫 번째 내부
명령 DIR을 출력하고 종료한다. 32~126사이의 출력 가능한 문자인지 검사하
기 위하여, iscntrl()을 사용하였다. 또한 문자열의 끝(End Of String)을 나타
내기 위해 DIR의 마지막에 '\0'을 추가한 것에 주목하라. 또한 테이블의 시작
을 찾기 위해 간단한 오토마타(Automata)■를 사용하였다.

             연속한 5문자를 찾기 위해 각각의 상태를 1,2,3,4와 5로 두어 프로그램을
          구현하였다. 이렇게, 문제를 해결하는데 사용한 상태 변화 모델을 오토마
          타라 한다. 오토마타에 관한 주제는 이 책에서 자세히 설명하지 않는다.

             오토마타: 내부 명령어 테이블의 시작을 발견하는 오토마타. 상태 5가되
          면, 테이블의 위치를 발견한 경우이다.

#include <stdio.h>

#include <ctype.h>

#include <io.h>

#include <fcntl.h>

#include <sys/stat.h>

void main(){

    int handle;

    unsigned char c=0;

    char s[80];

    int state=0;

    handle=open("f:\\document\\command.com",O_BINARY);

    if (handle==-1) return;//에러가 발생한 경우

    while (!eof(handle)){

        read(handle,&c,1);

        if (iscntrl(c))//0~31사이의 문자인가?

           printf(".");

        else

           printf("%c",c);

        switch(state){

        case 0:

            if (c==0x14) state=1;

            else state=0;

            break;

        case 1:

            if (c==0x56) state=2;

            else state=0;

            break;

        case 2:

            if (c==0x98) state=3;

            else state=0;

            break;

        case 3:

            if (c==0x87) state=4;

            else state=0;

            break;

        case 4:

            if (c==0x03) {//내부 명령어 테이블의 시작 발견

                read(handle,s,3);//read 'DIR'

                s[3]=0;//stuff EOS mark

                printf("%s",s);

                close(handle);

                return;

            } else state=0;

        }//switch

    }//while

    close(handle);

}//main

  출력 결과는

...DIR

이다.

  독자들은 위 프로그램의 늦은 속도에 불만인지 모른다. 이것을 빠르게 구현
할 수는 없을까? 물론 우리가 생각하는 모든 것의 해결책은 이미 존재한다.
버퍼링(buffering)을 사용하는 것이다!

   버퍼링(buffering)

  위의 프로그램이 속도가 늦은 이유는 파일에서 1바이트씩 읽는 비효율적인
코드 때문이다. 파일 입/출력 작업은 이렇게 해서는 안 된다. 되도록 디스크
접근 시간(disk access time)을 줄여야 한다. 조언은 다음과 같다.

"파일 입/출력에는 항상 버퍼링(buffering)을 사용하라."

  버퍼링이란 디스크 접근 시간을 줄이기 위해서, 프로그램에서 임시로 만든
버퍼(buffer)■에, 파일로부터 한꺼번에 많은 양의 데이터를 읽어들이는 방식을

             프로그램에서 특별한 용도로 할당된 메모리 블럭을 버퍼라 한다.

말한다. 일반적으로 버퍼의 크기는 2의 멱승(power of 2)으로 설정한다.

  버퍼의 크기를 4096(212), 버퍼로 읽어 들인 크기를 bp, 버퍼에서 현재 위치
를 cp가 가리킨다고 하자. 프로그램의 초기 상태는 bp=cp=0에서 시작한다.

             버퍼의 초기 상태: bp=cp=0이거나, cp>=bp가 될 때 버퍼는 새로운 내용으
          로 갱신되어야 한다.

  cp의 값은 버퍼에서 데이터를 읽을 때마다, 1씩 증가한다.

  프로그램에서 4096바이트를 읽으려고 시도할 때마다, 대부분의 경우에, 마지
막 경우를 제외하고 4096바이트가 모두 읽힐 것이다.

             버퍼가 꽉 찬 상태: 대부분의 경우에 버퍼에는 4096바이트가 모두 찬다
          (full).

  하지만, 파일의 마지막 부분에서는 4096바이트가 아닌 다른 값이 될 확률
(4095/4096)이 높다. 예를 들면, 마지막에 2002바이트가 읽혔다고 하자.

             파일의 마지막을 읽었을 때 버퍼의 상태: 파일의 마지막에서는 bp가 4096
          이 되지 않을 확률이 높다. 이 값을 2002(월드컵!)라 하자.

  몇 바이트가 버퍼에 들어있던지, cp가 bp를 넘어갈 수는 없다. cp가 bp를
넘어가는 조건은 파일에서 버퍼로 데이터를 읽어들여야 함을 의미한다.

             버퍼의 갱신 상태: 버퍼가 꽉 찼던(bp=4096), 꽉 차지 않았던(bp=2002), cp
          가 bp와 같아진다면, 버퍼의 내용을 갱신해야 한다.

  같은 소스를 버퍼링을 사용한 예를 아래에 리스트하였다. 파일의 끝을 나타
내기 위해 bEof란 플래그 변수를 사용하였다. read()가 읽은 바이트 수를 리
턴한다는 사실에 주목하라.

#include <stdio.h>

#include <ctype.h>//for 'iscntrl()'

#include <io.h>

#include <fcntl.h>

#include <sys/stat.h>

unsigned char buffer[4096];

int bEof=0,//EOF flag

    bp=0,//buffer size pointer

    cp=0;//current pointer

void ReadToBuffer(int handle) {

    cp=0;

    if (bp>0 && bp<4096) {//bp가 4096보다 작다면, 더 이상 읽어서는 안된

                          //다. bp>0 조건은 시작 상태를 구분하기 위해 사

                          //용하였다.

        bEof=1;

        return;

    }//if

    bp=read(handle,buffer,4096);//read는 읽은 바이트 수를 리턴한다.

}//ReadToBuffer

unsigned char ReadChar(int handle) {

    if (cp>=bp)//버퍼를 갱신해야 한다.

        ReadToBuffer(handle);

    return buffer[cp++];

}//ReadChar

void main(){

    int handle;

    unsigned char c;

    char s[80];

    int state=0;

    handle=open("f:\\document\\command.com",O_BINARY);

    if (handle==-1) return;

    while (1){

        c=ReadChar(handle);

        if (bEof) break;

        if (iscntrl(c))

            printf(".");

        else

            printf("%c",c);

        switch(state){

        case 0:

            if (c==0x14) state=1;

            else state=0;

            break;

        case 1:

            if (c==0x56) state=2;

            else state=0;

            break;

        case 2:

            if (c==0x98) state=3;

            else state=0;

            break;

        case 3:

            if (c==0x87) state=4;

            else state=0;

            break;

        case 4:

            if (c==0x03) {

                s[0]=ReadChar(handle);//이 함수는 개선이 필요하다.

                s[1]=ReadChar(handle);//연습문제를 참고하라.

                s[2]=ReadChar(handle);

                s[3]=0;//stuff EOS mark

                printf("%s",s);

                close(handle);

                return;

            } else state=0;

        }//switch

    }//while

    close(handle);

}//main

  출력 결과는

..DIR

이다. 하지만, 속도가 많이 개선되었다.

   위의 예에서는 버퍼의 크기를 4096으로 하였는데, 일반적으로 버퍼
의 크기는 얼마가 적당한가?

   파일 시스템에 따라 다르다. 몇 개의 2n을 시험해 보고, 적당한 값을
선택하라.

  실습문제

1. MFC(Microsoft Foundation Class)에서 지원하는 CFile과 같은 기능을 하
는 클래스를 제작하라.

2. command.com 내부 명령어 편집기 comchg.exe를 완성하라. 프로그램은 다
음과 같이 동작해야 한다. 예를 들면

comchg dir lst

은 DIR을 LST로 바꾼다.

3. 파일 입출력을 지원하는 풀 스크린 편집기(full screen editor)를 작성하라.

4. 위의 소스 중 win.ini파일을 출력하는 소스 코드를 버퍼링을 지원하도록 수
정하라.

5. 위의 소스 중 버퍼링을 구현한 소스에서 ReadChar()함수는 오직 1바이트
많을 읽는다. 여러 바이트를 읽는 ReadData()함수를 추가하라. 함수의 원형은

int ReadData(int handle,void* data)

이다.


19. 함수 포인터

19. 함수 포인터(function
pointer)

  함수 포인터가 필요한 경우

  다음과 같은 프로그램을 작성한다고 가정해보자. 사용자로부터 0,1 혹은 2를
입력받아 해당하는 함수를 호출하여 결과를 화면에 출력한다. 화면에는 다음
과 같이 출력한다.

    a=5,b=2

    Select a Function Number:

        0=Exponent(a,b): a**b

        1=Multiply(a,b): a*b

        2=Divide(a,b): a/b

  이 상황에서, 사용자가 0을 입력하면, 52을 출력하면, 1을 입력하면, 5×2=10
을 출력하며, 2를 입력하면, 5/2=2를 출력한다. 너무나 간단하다. 하지만 말하
려는 주제는 이것이 아니다. 하지만, 먼저 위의 프로그램을 구현해보자.

#include <stdio.h>

#include <math.h>

#include <conio.h>

long Exponent(long a,long b) {

    return pow(a,b);

}//Exponent

long Multiply(long a,long b) {

    return a*b;

}//Multiply

long Divide(long a,long b) {

    return a/b;

}//Divide

void main() {

    long a=5,b=2;

    int i;

    printf("a=5,b=2\n"

           "Select a Function Number:\n"

           "0=Exponent(a,b): a**b\n"

           "1=Multiply(a,b): a*b\n"

           "2=Divide(a,b): a/b\n");

    scanf("%d",&i);

    switch (i) {

        case 0:

            printf("%ld\n",Exponent(a,b));

            break;

        case 1:

            printf("%ld\n",Multiply(a,b));

            break;

        case 2:

            printf("%ld\n",Divide(a,b));

            break;

    }

    getch();

}

  함수 포인터 선언

  이 상황에서 먼저 함수 포인터에 대해 알아보자. 함수 포인터는 포인터 변
수의 특수한 종류인데, 함수의 시작 주소를 가지는 포인터 변수이다. 정수형의
포인터 변수를 선언하려면,

    int *i;

라고 하면 된다. 그러면 i에는 정수의 주소 값은 무엇이든 대입할 수 있다. 예
를 들어,

    int *i;

    int j=2;

    i=&j;

같은 문장이 가능하다. 먼저 우리가 주목해야 하는 사실은 함수의 끝에 ()를
적어주지 않으면 그것이 무엇을 의미하는가 이다. 함수의 끝에 붙는 ()를 '함
수 호출 연산자(Function Call Operator)'라고 한다. 즉 함수의 끝에 ()를
써 주어야지 실제의 함수 호출이 일어난다. 그러면 아래의 문장은 무엇이 잘
못일까?

    int i;

    i=getch;

즉 사용자의 실수로 i=getch()를 잘못 적은 것이다. i=getch; 이 문장은 getch
함수의 시작 주소를 i에 대입하려고 시도한다. 하지만 i는 정수형의 변수이므
로 실패한다. 즉 컴파일 시간에 에러가 발생한다. 그러면 i를 어떤형의 변수로
선언해야 올바른 문장이 될까? 바로 i를 함수를 가르키는 포인터(Function
Pointer), 즉 함수 포인터로 선언해야 한다. 하지만 우리가 주의해야 하는 것
은 함수의 포인터를 제대로 초기화하기 위해서는 함수의 원형(Prototype)을
제대로 알고 있어야 한다는 것이다. 함수의 원형을 모르고 있으면 함수를 호
출할 수 없으므로, 함수의 포인터에 함수의 원형에 관한 모든 정보가 포함되
어야 한다.

  수정할 문장은 다음과 같다.

    int (*i);

이 문장은 i가 정수형 포인터가 아니라 정수를 리턴하는 함수형 포인터라는
것을 의미한다. 여기에 함수의 원형 정보를 추가해야 한다. 만약 이 함수 포인
터에 정수를 두개 파라미터로 받는 임의의 함수 f(int a,int b); 의 시작주소로
초기화를 원한다면,

    int (*i)(int,int);

라고 선언해야 한다. 이 선언은 i가 정수를 리턴하면서, 정수 2개를 파라미터
로 받는 함수의 시작 주소를 대입할 수 있음을 의미한다. 그러므로 getch()의
경우,

    int (*i)();

라고 선언해야 한다. 그러면 i에는 정수를 리턴하면서 파라미터는 없는 임의
의 함수의 시작 주소를 대입할 수 있다. 그러므로, i에 getch()함수의 시작 주
소를 대입하려면,

    int (*i)();

    i=getch;

라고 하면된다. 여기서 여러분이 주의해야 할 것은 i는 함수의 시작 주소이지
함수 자체가 아니라는 것이다. 그러므로

    i();

는 타당한 함수 호출이 아니다. i가 가리키는 내용이 함수이므로,

    *i

가 함수를 의미한다. 그러므로 여기에 함수 호출 연산자를 붙여서,

    (*i)();

가 함수 호출이 된다. 그러므로,

    int (*i)();

    int j;

    i=getch;

    j=(*i)();

는 키보드로부터 한 개의 문자를 입력받는

    j=getch();

와 같다.

  함수 포인터 배열

그렇다면 함수 포인터의 배열은 어떻게 선언할까? 일반 배열이 그런 것처럼
이름 바로뒤에 []을 붙여주어야 한다. 그러므로,

    int (*i[3])();

은 타당한 배열 선언이다. 즉 정수를 리턴하는 함수의 주소를 받을 수 있는
크기 3인 함수 포인터 배열을 선언한 것이다. 이 사실을 알면, 위에서 제시한
프로그램 소스를 다음과 같이 고칠 수 있다.

#include <stdio.h>

#include <math.h>

#include <conio.h>

long Exponent(long a,long b) {

    return pow(a,b);

}//Exponent

long Multiply(long a,long b) {

    return a*b;

}//Multiply

long Divide(long a,long b) {

    return a/b;

}//Divide

void main() {

    long (*f[])(long,long)={Exponent,Multiply,Divide};

    long a=5,b=2;

    int i;

    printf("a=5,b=2\n"

           "Select a Function Number:\n"

           "0=Exponent(a,b): a**b\n"

           "1=Multiply(a,b): a*b\n"

           "2=Divide(a,b): a/b\n");

    scanf("%d",&i);

    printf("%ld\n",(*f[i])(a,b));

}

  긴 switch 문장이 간단하게 한 줄로 준 것이다. 그렇다면 함수 포인터는 이
렇게 이용하는 것이 고작인가? 아니다, 훨씬 더 좋은 응용이 가능하다.

  여러분이 구현하려는 어떤 함수가 매번 같은 일을 대부분 하지만 마지막 순
간에만 다른 일을 하는 함수가 있을 수 있다. 예를 들어 10개의 함수가 앞의
95%는 같고, 뒤의 5%만이 다른 것이다. 그렇다면 여러분은 앞의 95%를 별개
의 함수로 만들어 10개의 함수속에서 그 특별한 별개의 함수를 매번 호출할
것이다.

  하지만, 함수 포인터를 아는 이 상황에서 전혀 다른 접근이 가능하다. 즉 1
개의 함수를 만들 때, 파라미터로 함수 포인터를 받도록 하는 것이다. 그러면,
여러분은 그 함수의 내부에서 95%의 일을 한 다음, 마지막 5%를 위하여 파
라미터로 넘어온 함수의 포인터를 이용하여 그 함수를 호출하면 되는 것이다.
즉 번거롭게 switch문 따위를 이용하여, 내부를 구현할 필요가 없어지는 것이
다.

  그렇다면 무엇이 이익인가? 지금 예들은 이러한 상황에서 이익은 없다. 하
지만 여러분은 분명히 이익이 되는 어떤 상황을 다른 책들에서 발견할 수 있
을 것이다. 하지만, 여러분이 알고 있어야 할 것은 함수 포인터를 전혀 사용하
지 않고도 그 문제를 해결할 수 있다는 것이다.

  오버로드된 함수의 주소

  C++에서 다형성(polymorphism)은 파라미터의 형식이 다른, 같은 이름의 함
수를 허용한다. 이러한 오버로드된 함수의 주소를 얻는 것은 C와 다르지 않
다. 함수의 주소를 얻기 위해서는 파라미터의 개수와 형을 모두 명시해야 하
므로, 컴파일러는 어느 함수를 대입해야 할지 판단할 수 있다. 아래의 예제를
참고하라.

#include <stdio.h>

void Print(int i) {

    printf("%d\n",i);

}

void Print(char* format,int i) {

    printf(format,i);

}

void main() {

    void (*f)(char*, int);

    f=Print;//f의 정보는 두 번째 Print의 주소가 대입될 것을 알려준다.

    (*f)("hello %d\n",100);

}

  위의 예에서 f에는 두 번째 Print(char *, int) 함수의 주소가 대입된다. 출력
결과는

hello 100

이다.

  디폴트 파라미터

  C++에서 함수 포인터에 추가된 사항은 또한 디폴트 파라미터에 관한 것이
다. 비록 원래의 함수가 디폴트 파라미터를 가지지 않더라도 함수 포인터에
디폴트를 선언하는 것을 허락한다. 아래의 예제는 오버로드된 두 번째 Print()
함수의 두 번째 파라미터에 디폴트 값을 지정하고 있다. 이러한 기교는 디폴
트 값을 가지지 않는 표준함수에 디폴트를 추가하는 것을 - 함수를 새로 작
성하는 부담 없이 - 구현 가능하게 해 준다.

...

void main() {

    void (*f)(char*, int i=01));

  1) 형식 파라미터를 적어주어야 한다. int=0과 같은 문법은 불가능하다.

    f=Print;//f의 정보는 두 번째 Print의 주소가 대입될 것을 알려준다.

    (*f)("hello %d\n");

}

  결과는 물론

hello 0

이다.

  독자들은 표준함수 itoa()를 알고 있을 것이다. 이것은 정수(integer)를
ASCII 스트링을 변환하는 표준함수이다. 이것을 사용하기 위해서는 stdlib.h를
포함(include)해야 한다.

#include <stdlib.h>

...

char *itoa(int value, char *string, int radix);

...

  itoa()의 마지막 파라미터는 변환을 원하는 진수의 베이스(base)이다. 만약
10진수로 변환을 원한다면, 세 번째 파라미터를 10으로 설정해야 한다. 이것을
세 번째 값의 디폴트가 10인 함수 포인터로 구현하려고 한다. 어떻게 해야 하
는가? 이 문제는 연습문제로 남긴다.

  함수 포인터 형의 정의

  함수 포인터를 빈번하게 사용해야 한다면, typedef■를 이용하여 형을 정의

  typedef(type definition)는 형을 정의하기 위해 사용하는 키워드이다.

하는 것이 바람직하다. 함수 포인터 형의 정의는 좀 특이하다. typedef가 없다
면 이것은 함수 포인터 변수의 선언과 동일하다.

typedef long (*Fun)(long,long);

  위에서 정의된 형(type)은 Fun이다. 이제 Fun은 long을 리턴하고, 파라미터
로 2개의 long을 가지는 함수 포인터 변수를 선언하기 위해 형으로 동작할 수
있다. 그러므로 Fun형의 배열은

Fun f[3];

처럼 선언하는 것이 가능하다. f[0], f[1], f[2]는 각각 함수 포인터 변수이다.
어떤 독자들은 typedef의 형식이 아래처럼

typedef long (*Fun)(long,long) Func;

이지 않은 것이 불만일지도 모른다. 하지만, 위의 형식은 컴파일 시간 에러를
발생한다. 독자들은 Fun이 태그 이름으로 사용되기를 바랬지만, Fun은 형 이
름이다. 그러므로 Func를 사용하는 것은 에러이다.

  이 형식이 구조체에서의 형식과 혼동되는 것은 사실이다.

typedef struct People {

    int age;

    char name[80];

} SPeople;

    에서 위처럼 선언했을 때, 새로 정의된 형 이름은 SPeople이였다.
People은 꼬리표 이름(tag name)이라고 하는데, 자체는 아무 의미가 없으며,
struct와 함께 사용하면 형을 나타낸다. 그러므로   에서는 아래와 같이 사
용하는 것이 가능했었다.

#include <stdio.h>

#include <conio.h>

typedef struct People {

    int age;

    char name[80];

} SPeople;

void main() {

    SPeople seo={30,"seojt"};

    struct People yoon={27,"yoonmk"};

    printf("%d,%s\n",seo.age,seo.name);

    printf("%d,%s\n",yoon.age,yoon.name);

}

  하지만,   에서 꼬리표 이름은 더 이상 의미 없는 것이 아니다. tag name
은 혼자서 형 이름으로 동작한다. 그리고 C와의 호환성을 위해서

    SPeople seo;

    struct People seo;

    People seo;

처럼 선언하는 것이 모두 가능하다. 하지만 struct People보다는 People을 단
독으로 사용하는 것이 일반적이다.

#include <stdio.h>

#include <conio.h>

struct People {

    int age;

    char name[80];

};

void main() {

    People seo={30,"seojt"};

    People yoon={27,"yoonmk"};

    printf("%d,%s\n",seo.age,seo.name);

    printf("%d,%s\n",yoon.age,yoon.name);

}

  구조체와 공용체의 꼬리표 이름과는 달리, 함수 포인터 선언에 꼬리표 이름
은 존재하지 않는다. 그러므로, typedef long (*Fun)(long,long) Func; 처럼
선언하는 것은 에러이다.

  typedef를 이용하여 수정된 소스는 아래와 같다. 변경된 부분은 굵게 표시
하였다.

#include <stdio.h>

#include <math.h>

#include <conio.h>

typedef long (*Fun)(long,long);//Fun is function pointer type

long Exponent(long a,long b) {

    return pow(a,b);

}//Exponent

long Multiply(long a,long b) {

    return a*b;

}//Multiply

long Divide(long a,long b) {

    return a/b;

}//Divide

void main() {

    Fun f[]={Exponent,Multiply,Divide};

    long a=5,b=2;

    int i;

    printf("a=5,b=2\n"

           "Select a Function Number:\n"

           "0=Exponent(a,b): a**b\n"

           "1=Multiply(a,b): a*b\n"

           "2=Divide(a,b): a/b\n");

    scanf("%d",&i);

    printf("%ld\n",(*f[i])(a,b));

}

  멤버 함수의 주소

  객체마다 멤버 함수가 만들어지는 것은 아님에도 불구하고, 멤버 함수가 파
라미터로 받는 this 포인터 때문에 멤버 함수의 주소를 얻는 방법과, 멤버 함
수의 주소 변수(member function pointer)를 선언하는 방법은 일반 함수의 것
과는 다르다.

  int를 리턴하고, void를 파라미터로 받는 CTest의 멤버 함수 포인터를 선언
하기 위해서는

int (CTest::*fp)();

와 같이 선언한다. 굵게 표시된 CTest는 반드시 명시되어야 한다. CTest:: 표
기가 일반 함수의 포인터와 멤버 함수의 포인터를 구분 짓는다. 이 선언이
CTest의 멤버 변수로써의 선언이 아니라는데 유의하라. 즉 위의 선언은
CTest의 멤버 함수의 포인터를 가질 수 있는 일반 변수 - 클래스의 멤버 변
수가 아닌 - fp를 선언한 것이지, CTest의 멤버 변수 fp를 선언한 것은 아니
다. 그러므로 아래와 같은 대입문은 모두 타당하지 않다(CTest의 객체 t가 만
들어졌으며, CTest는 멤버 함수 Get()을 가진다고 가정하자).

    ...

    t.fp=t.Get;//error! fp는 t의 멤버가 아니다.

    CTest::fp=t.Get;//error! fp는 CTest의 static 변수가 아니다.

  만약 CTest의 객체 t가 만들어졌다면, t의 멤버 함수 Get()의 시작 주소를
대입하기 위해

fp=t.Get;

이라고 사용하는 것 역시 불가능하다. fp는 CTest 클래스의 멤버 함수의 주소
를 대입하기 위해 선언된 것이지, 객체 t의 - 비록 그것이 CTest의 객체라도
- 멤버 함수의 주소를 대입하기 위해 선언된 것이 아니다. 그러므로 CTest
클래스의 멤버 함수의 주소는 아래와 같이 대입한다.

fp=CTest::Get;■

            fp=&CTest::Get; 처럼 &를 사용하는 것도 가능하며, 이것은 주소를 대입하
          는 것을 명확히 한다. 이것은 프로그래머의 취향이며, 필자는 전자를 좋아
          한다.

아래의 예제를 참고하라.

#include <iostream.h>

class CTest {

    int i;

public:

    CTest(int t=0) { i=t; }

    int Get() { return i; }

    void Set(int t) { i=t; }

};

int (CTest::*fp)();//변수 fp는 CTest의 멤버 함수의 주소를 가질 수 있

                     //다.

void main() {

    CTest t(5);

    fp=CTest::Get;//fp는 CTest 클래스의 Get() 멤버 함수의 시작 주소

                   //를 가진다.

    cout << (t.*fp)() << endl;//비록 fp가 CTest의 멤버 변수는 아니지

                              //만, 어느 객체의 Get() 멤버 함수인지를

                              //명확히 알아야 하므로, 객체 이름과 .*

                              //연산자를 사용해야 한다.

}//main

  위의 예제의 출력 결과는

5

이다.

  실제로 위의 프로그램이 제대로 실행된다는 것은 다소 놀랍다. 의문은 다음
과 같다.

   fp는 t의 멤버가 아닌데 왜 t.*fp처럼 사용하는 것이 가능한가?

C++의 새로운 연산자: .*와 ->*

  이것에 관한 대답은 C++의 새로운 연산자 .*와 ->*의 기능 때문이다. 이
두 연산자는 멤버의 주소를 참조하기 위해 새롭게 추가된 연산자이다. 일반적
으로 점(.)과 화살표(->)는 객체의 멤버를 참조하기 위해 사용되지만, .*와
->*는 객체에서 객체의 멤버가 아닌 변수를 참조하기 위해 사용된다.

  아래의 예제를 참고하라. ->*는 객체 포인터가 선언된 경우 사용한다.

#include <iostream.h>

class CTest {

    int i;

public:

    CTest(int t=0) { i=t; }

    int Get() { return i; }

    void Set(int t) { i=t; }

};

int (CTest::*fp)();

void main() {

    CTest t(5);

    CTest* tp=&t;

    fp=CTest::Get;

    cout << (tp->*fp)() << endl;//fp는 tp가 가리키는 객체의 멤버가 아니

                                 //지만, ->* 연산자를 사용하여 참조가 가

                                 //능하다.

}//main

  실제로 fp에는 무엇이 할당되는가?

  클래스의 멤버 변수는 객체가 만들어질 때마다, 메모리에 각각이 할당
된다. 하지만, 클래스의 멤버 함수는 객체마다 만들어지는 것이 아니라 - 심
지어 함수의 포인터조차도(가상 함수 포인터를 제외하곤) 만들어지지 않는다
- 클래스마다 유지된다. 그러므로 컴파일 시간에 멤버 함수의 주소를 유일하
게 결정할 수 있다. 그러므로 fp=CTest::Get; 같은 초기화가 가능하다. 오히려
fp=t.Get; 같은 초기화는 객체의 주소가 알려지지 않으므로, 불가능하다. sizeof
를 사용하여 클래스 객체의 크기를 출력해 보라. 멤버 변수가 추가되면 크기
또한 증가한다. 하지만, 멤버 함수가 추가된다고 해서 객체의 크기는 증가되지
않는다. 아래의 예제를 참고하라. 모두의 결과는 2이다.

#include <iostream.h>

class A {

    char c;

    char d;

public:

    A(){ c=d=0; }

    void SetC(int t) { c=t; }

    void SetD(int t) { d=t; }

};//class A

class B {

    char c;

    char d;

public:

    B(){ c=d=0; }

    void SetC(int t) { c=t; }

    void SetD(int t) { d=t; }

    void PrintC() { cout << c << endl; }

    void PrintD() { cout << d << endl; }

    void PrintCD() { cout << c << " " << d << endl; }

};//class B

void main() {

    A a;

    B b;

    cout << sizeof a << endl;

    cout << sizeof b << endl;

}//main

  멤버 함수 주소의 응용

  멤버 함수의 주소가 잘 응용된 곳은 C++ 표준 라이브러리인 iostream에서
찾아 볼 수 있다. iostream에는 전역 객체인 cout과 cin을 가지고 있는데, 이
것은 C의 printf()와 scanf()를 대치한다.

printf("hello\n");

에 대응하는 cout의 표현은

cout << "hello\n";

이다. 또한 조작자(manupulator)라고 불리는 특별한 endl() 기능을 이용하여

cout << "hello" << endl;

처럼 사용하는 것이 가능하다. 위의 문장에서 endl은 줄을 바꾸는 기능을 한
다. 어떻게 이것이 가능한가? C++를 처음 접했을 때, endl의 이상한 기능에
놀란적이 있다. 이 비밀은 바로 함수 포인터에 있었다■.

            실제로 필자가 endl이 함수임을 확인한 바는 없다. 하지만, 이것이 enum상
          수나 static 상수가 아닌 것은 분명하며(상수라면 ostream::endl 의 형식으로
          사용할 것이다), 조작자인 것도 분명하다. 그리고 조작자는 멤버 함수인 것
          도 분명하다(사용자가 조작자를 정의할 때 멤버 함수를 정의하므로). 그러
          므로 endl은 함수이다. 지금 당장 iostream.h를 찾아보라.

  endl()은 단순히 cout의 멤버 함수이다. 그러므로

endl

표현은 멤버 함수의 주소를 의미한다. cout에는 멤버 함수의 주소를 받는 특
별하게 오버로드된 << 연산자가 있다. 소스를 본 적은 없지만, 아마도 멤버
함수의 주소를 받는 오버로드된 << 연산자의 내부에서는 파라미터로 넘어온
멤버 함수를 호출할 것이다. 또한 표현식이 LValue에 위치할 수 있어야 하므
로, ostream&를 리턴할 것이다.

  이런 기능을 iostream에서는 조작라는 명칭으로 추상화하고 있다■. 조작자

            필자는 개인적으로 지저분한 - 사용자가 많은 시간을 투자해야 알 수 있
          는 - iostream 표준 라이브러리를 싫어한다. 이런 상황에서 STL이라니 맙
          소사! 우리가 배워야 하는 모든 것은 C++이지 남들이 만들어 놓은 - 비록
          그것이 잘 만들어 졌더라도 - 지저분한 라이브러리가 아니다. 그럼에도 불
          구하고 이 책의 몇몇 예제에서 iostream의 간단한 기능을 이용하고 있는데
          그것은 단순히 타이핑이 귀찮아서이다. printf("hello\n"); 를 타이핑 치는 것
          보다, cout << "hello" << endl; 를 타이핑 치는 것이, 적게 shift를 눌러도
          되므로 조금 편하다.

가 중요한 것은 아니지만, 함수 포인터가 잘 사용된 예를 적절하게 보여주고
있다.

  iostream을 전혀 사용하지 않고, 이 기능의 일부 - cout이란 전역 객체와
조작자 endl을 사용한 - 를 모방한 아래 예제를 참조하라.

#include <stdio.h>

class ostream;

typedef void (*Manipulator)();

void endl() {

    printf("\n");

}//endl

class ostream {

public:

    ostream& operator<<(char* s);

    ostream& operator<<(Manipulator m);

};//class ostream

ostream& ostream::operator<<(char* s) {

    printf("%s",s);

    return *this;

}//ostream::operator<<

ostream& ostream::operator<<(Manipulator m) {

    (*m)();

    return *this;

}//ostream::operator<<

ostream cout;

void main() {

    cout << "hello" << endl << "world" << endl;

}//main

  출력 결과는

    hello

    world

이다. 위의 문장이 처리되는 과정은 다음과 같다.

             cout << "hello": 이 문장은 ostream& operator<<(char* s);를 호출한다.
          ostream의 레퍼런스가 리턴되므로, cout << "hello"는 cout자체이다.

             cout << endl: 이 문장은 ostream& operator<<(Manipulator m);를 호출한다.
          역시 ostream의 레퍼런스가 리턴되므로, cout << endl 역시 cout자체이다.

             cout << "world": 이 문장은 ostream& operator<<(char* s);를 호출한다.
          ostream의 레퍼런스가 리턴되므로, cout << "hello"는 cout자체이다.

             cout << endl: 이 문장은 ostream& operator<<(Manipulator m);를 호출한다.
          역시 ostream의 레퍼런스가 리턴되므로, cout << endl 역시 cout자체이다.

명시적 호출(explicit call)

  위의 예에서 오버로드된 연산자 <<의 명시적 호출 버전을 작성할 수 있다.
다음은

cout << "hello";

명시적 호출을 사용하면

cout.operator<<("hello");

이다. 그러므로

cout << "hello" << endl << "world" << endl;

의 명시적 호출은

(((cout.operator<<("hello")).operator<<(endl)).operator<<("world")).operator<
<(endl);

이다. 이것은 실제로 컴파일러가 오버로드된 <<를 해결하는 방법이다. 명시적
호출에 관한 주제는 '연산자 오버로딩'에서 자세히 다룬다.

  ostream의 조작자는 함수 포인터의 강력한 응용을 잘 보여준다. 하지만, 독
자들은 함수 포인터의 더 강력한 응용을 '가상 함수'에서 찾알 볼 수 있을 것
이다.

   실습문제

1. STL(standard template library)의 어떤 부분은 반드시 함수 포인터로 구현
해야 하는 상황이 발생한다. 어떤 부분이 그러하며, 왜 그런가?(Hint: iterator)

2. itoa(int value, char *string, int radix)의 세 번째 파라미터 radix가 디폴트
값 10을 가지도록 하는 함수 포인터를 선언하라.

3.


20. 전처리 명령어(preprocess command)

20. 전처리 명령어(prepro
cessing command)

  전처리 명령어(preprocessing command)는 C의 키워드이지만, 좀 특이한
명령어이다. 컴파일러가 C 소스를 기계어 코드로 번역하는 것을 처리
(processing)라 한다. 전처리 명령어는 이러한 '처리'전에(pre) 실행되는 명령
어이다.

  전처리 명령어를 일반 처리 명령어(?)와 구분하기 위해서 전처리 명령어는
모두 특수문자 파운드(#)■로 시작한다. 아래는 사용 가능한 모든 전처리 명령

             #의 원래 발음은 pound이다. 하지만, 이것을 number(#) 혹은 겼(sharp)이
          라 발음해도 - 한국에선 - 무방하다.

어이다(모두 #으로 시작함을 알 수 있다).

① #include

② #define

③ #if

④ #ifdef

⑤ #ifndef

⑥ #elif

⑦ #else

⑧ #endif

⑨ #undef

⑩ #line

⑪ #error

⑫ #pragma

  전처리 명령어 중, #include, #define, #ifndef 와 #endif는(굵게 표시된) 대부
분의 프로그램에서 빈번하게 사용되는 중요한 명령어이므로, 반드시 사용법을
확실하게 익혀두어야 하며, 다른 명령어는 많이 사용되지는 않는다.

  리스트 중, ③∼⑧번의 전처리 명령어들은 - 조건 컴파일 등을 위해 - 일반
적으로 함께 사용하는 명령어들이다.

   #include

  #include 명령문■은 소스에 다른 소스파일을 '끼워넣기'하기 위해 사용한다.

             실제로 이것은 명령문(command)이 아니라, 전처리 명령어(preprocessing
          command)이다. 하지만, 문맥이 명확하다면, 명령어라고 사용한다.

HWP™의 끼워넣기 기능인 ^O의 기능과 동일하다.

  소스 파일에 다른 소스 - 그것이 *.h 이든, *.cpp 이든 - 를 끼워넣는 과정
을 생각해 보라. 이것은 분명 원래의 소스가 기계어 코드로 번역이 되기 전에
처리되어야 한다. 그래서 #include 문은 전처리 명령어이다. 실제로 #include
문은 컴파일 전에 이루어진다.

  #include 문의 문법은 다음과 같다.

    #include <header_name>

    #include "header_name"

    #include macro_identifier

  아래 그림과 같이 3개의 소스 파일로 이루어진 프로그램을 생각해 보자.

             3개의 파일로 이루어진 프로젝트: 전체 프로그램을 구성하는 소스 파일이
          1개 이상인 경우 이것을 결합하는 것이 필요하다. macro.h의 #define에 대
          해서는 아래의 절에서 자세히 설명한다. 지금은 단순히 PRINT(i)가
          printf("%d",i)를 의미한다고 알아두자.

  소스 파일이 1개 이상인 경우, 원하는 실행 파일을 만들기 위해서는 프로젝
트 파일(project file)을 만들어야 한다. 이것은 '프로젝트 만들기'에서 설명한
다. 하지만, 프로젝트 파일을 만들지 않고도 #include를 이용하여, 실행 파일을
만드는 것이 가능하다.

  위의 예에서 a.cpp의 main()에서 b.cpp의 f()를 호출하고 있으며, 또한
macro.h의 PRINT() 매크로를 호출하고 있다. 함수나 매크로 함수 모두 쓰기
전에 선언되어야 하므로, a.cpp의 f()와 PRINT() 모두 에러를 유발한다.

  해결 방법은 b.cpp의 소스와 macro.h의 소스를 그대로 a.cpp에 복사하여 사
용하는 것이다. #include는 이러한 작업을 컴파일러가 코드 생성 전에 하도록
지시한다. a.cpp가 macro.h와 b.cpp를 자신의 상단 부분에 복사하기 위해 다음
의 두 줄을 a.cpp의 소스에 추가한다.

    #include "macro.h"

    #include "b.cpp"

그러면 a.cpp의 소스는 다음과 같다.

#include <stdio.h>

#include "macro.h"

#include "b.cpp"

void main() {

    int i;

    i=f(2);

    PRINT(i);

}

  실제로 위의 소스는 컴파일 전에 아래와 같이 매크로 확장(macro
expansion)■된다.

             매크로 명령이 전처리 프로세서(preprocessing processor or macro
          processor)에 의해 처리되어 소스가 확장되는 것을 매크로 확장이라 한다.

#include <stdio.h>//이 문장은 일단 보류하자.

#define PRINT(i) printf("%d",i)

int f(int i) {

    return i*i*i;

}

void main() {

    int i;

    i=f(2);

    PRINT(i);

}

  그리고 실제의 소스는 #define이 완전히 확장되어 컴파일 전의 완전한 소스
는 아래와 같다.

#define <stdio.h>//실제로는 stdio.h가 이 부분에 확장될 것이다.

#define PRINT(i) printf("%d",i)

int f(int i) {

    return i*i*i;

}

void main() {

    int i;

    i=f(2);

    printf("%d",i);

}

             #include의 역할: #include는 소스 파일의 중간에 다른 소스 파일을 포함
          (include)할 수 있다.

  위의 예에서 편의 상 stdio.h의 확장은 소스에 고려하지 않았다. 독자들의
예상대로 특정한 소스 파일을 포함하는 전처리 명령은 #include 다음에 C의
문자열 표현을 사용하여 이중 인용 부호(double quotation mark: ") 사이에 파
일의 경로를 명시하면 된다.

  예를 들면, 현재 경로(current path)의 sub.h를 삽입하려면,

#include "sub.h"

라고 사용한다. 만약 sub.h의 경로가 C:\BorlandC\Bin\MyProject\ 라면,

#include "C:\BorlandC\Bin\MyProject\sub.h"

처럼 사용한다. 독자들은 #include가 명령문이 아니라, 전처리 명령문이므로,
위의 문장을 C의 Escape Sequence를 사용하여,

#include "C:\\BorlandC\\Bin\\MyProject\\sub.h"

처럼 적어서는 안된다. 기억하라. 전처리 명령문은 명령문이 아니므로, C에서
스트링을 표현하기 위한 ESC 절차를 사용하지 않는다.

  그러면,

#include <stdio.h>

는 무엇을 의미하는가? 흔히 C의 초보자들 - 혹은 숙련된 사람조차도 -
#include <...>는 시스템 헤더(system header file)를 포함하기 위해서,
#include "..."는 사용자 정의 헤더(user defined header file)를 포함하기 위해
서 사용하는 것이라고 알고 있지만, 이것은 틀렸다. 정답은 다음과 같다.

"<...>는 미리 정해진 경로(predefined path)의 헤더를 포함하기 위해서
사용하며, "..."는 사용자가 헤더 파일의 경로를 모두 지정(user specified
path)해 주어야 하는 경우에 사용한다."

  예를 들어 Borland C++ 3.1의 경우를 보자. BC++ 3.1은 메인 메뉴의
Options->Directories에서 미리 정해진 경로를 설정하는 대화상자를 제공한다.

             Borland C++ 3.1의 Options 메뉴: Directories 항목을 선택하면, #include 의
          미리 정해진 경로, 링크시에 필요한 라이브러리의 경로 및 결과가 만들어
          질 경로등을 설정할 수 있다.

 대화 상자는 아래와 같다.

             Include Directories: #include 의 미리 정해진 경로는 Include Directories 에
          서 설정한다. 그림의 경우 E:\BorlandC\Include가 설정되어 있다.

  대화상자에서 사용자는 4개의 글 상자(text box)에 적당한 값을 입력한다.
각각은 #include의 디폴트 폴더의 경로, 링크시에 필요한 라이브러리의 경로,
*.exe와 *.obj가 만들어질 경로, File->Open을 선택했을 때의 소스 파일의 경
로를 명시한다. 위의 예에서 #include의 디폴트 경로는 E:\BorlandC\Include이
다.

  그러므로

#include <stdio.h>

는

#include "E:\BorlandC\Include\stdio.h"

와 동일하다. 일반적으로 디폴트 경로에는 시스템에서 사용하는 헤더 파일이
위치하므로, 위의 사실을 알고있다면, '#include <...>는 시스템 헤더를 포함
하는 문장이다.'라고 말해도 무리는 없다. 하지만, sub.h가
E:\BorlandC\Include에 위치한다면,

#include <sub.h>

라고 사용해도 틀리지 않다(sub.h는 시스템 헤더파일이 아니다).

  위의 소스에서 발생할 수 있는 좀 더 미묘한 문제를 살펴보기 전에 #define
에 대해 알아보자.

   #define

  #define은 매크로 상수와 함수를 정의(macro definition)하기 위해서 사용
한다. #define의 문법은 다음과 같다.

    #define macro_identifier <token_sequence>

    #define macro_identifier(<arg_list>) token_sequence

  예를 들면

    #define PI 3.141592

는 PI를 3.141592로 정의한 것이다. 그러므로 소스에서 PI는 컴파일 전에
3.141592로 치환된다. 즉, 소스의

printf("%f\n",PI);

는 컴파일전에

printf("%f\n",3.141592);

로 치환되어 번역될 것이다. 이 때 PI는 상수 3.141592처럼 사용되었으므로 PI
를 매크로 상수(macro constant)라 한다.

  매크로 상수를 정의할 때 주의해야 하는 사항은 단어 사이의 공백에 관한
것이다. 예를 들면 아래의 문장은

#define A B C

A B를 C로 정의하는 것인가? A를 B C로 정의하는 것인가?

  독자들이 문법을 주의 깊게 보았다면, 이것은 A를 B C로 정의한다는 것■

             명칭(identifier) - 변수 이름, 함수 이름 등 - 에는 공백이 올 수 없다.

을 알았을 것이다. 즉 #define은 #define 이후의 첫 번째 단어(공백으로 구분된
다)를 나머지 문자열로 정의한다.

  아래의 소스를 참고하라. #define은 언어를 전혀 다른 것처럼 만들 수도 있
다.

#include <stdio.h>

#define PI          3.141592

#define A           B, C

#define begin       {

#define end         }

#define procedure   void

int B=1,C=2;

procedure PrintPI()

begin

    printf("%f\n",PI);

end

void main()

begin

    PrintPI();

    printf("%d,%d\n",A);

end

  결과는

    3.141592

    1,2

이다.

  독자들은 C에서 블락(block)을 나타내기 위해 {와 }를 사용한다는 것을 알
고있을 것이다. Pascal 언어■에서 블락은 begin과 end로 나타낸다. 또한

             최초에 교육용 언어로 개발된 파스칼은, 알고리즘을 나타내기 위해서, 문
          법의 간단함 때문에, 80년대 후반까지 굉장한 인기를 누렸던 언어이다. 아
          직 파스칼은 Delphi에 Object Pascal로 포함되어 변함없는 인기를 누리고
          있다.

Pascal 언어에서 void형 함수는 프로시저(procedure)라고 한다.

  이제 #include의 마지막 문법을 살펴 볼 준비가 되었다.

    #define files "c:\borlandc\bin\macro.h"

    #include files

는

    #include "c:\borlandc\bin\macro.h"

와 같다.

   위의 소스에서 왜 PI를 3.141592로 정의했을까? 실제 π는 무한 소
수이므로, 유효자리를 모두 나타내는 것이 불가능하다. 하지만, 유효자리 8째
자리까지의 표현은 3.14159265이므로, 만약 매크로를 소수점 8째 자리까지 정
의하면, 결과는 얼마일까?

   %f는 정밀도를 소수점 6째 자리까지밖에 표현하지 못한다. 하지만,
매크로 상수는 이 제약이 없다(매크로는 단지 치환될 뿐이다). %f가 왜 정밀
도가 소수점 6째 자리까지인지는 '이진수'를 참고하라. 실제 Borland C++ 사
용자는 PI를 정의해서 사용하지 않고, math.h에 정의된 M_PI 매크로 상수를
사용한다.

왜 매크로 상수를 사용하는가?

  왜 매크로 상수를 사용하는가? 그것은 다음과 같은 몇 가지 이점 때문이다.

    ① 상수에 비해 메모리를 차지하지 않는다.

    ② 프로그램을 읽기 좋게 만든다.

    ③ 프로그램의 유지, 보수를 쉽게 만든다.

① #define PI 3.141592를 상수 표현식을 써서,

const double PI=3.141592;

라고 하면, 메모리를 4바이트 차지하게 된다. 물론 단점도 존재한다. 디버깅시
에 매크로 상수 PI는 관찰할 수 있는 값이 아니다(모두 치환되어 있을 것이므
로).

② 반지름 5인 원의 둘레를 계산하기 위해

float r=2*3.141592*5.0;

이라고 사용한 문장을 생각해 보라. 얼마나 코드를 읽기 어렵게 만드는가! 위
의 경우 PI는 대부분의 사용자가 알고 있는 알려진 상수이기 때문에 정도가
덜하다.

  어떤 프로그램이 자신의 상수 0.0012345678을 사용한다고 해보자. 이 상수는
샐러리 맨(salary man)의 월급에서 원천 징수되는 세금의 비율이라고 하자(이
나라는 얼마나 적은 세금을 징수하는가!). 샐러리 맨의 월급 pay에서 세금을
계산하기 위해서

    tax=pay*0.0012345678;

를 사용하는 것보다,

    #define RATIO    0.0012345678

    tax=pay*RATIO;

를 사용하는 것이 확실히 읽기 좋다■.

             소프트웨어 공학(software engineering)에서 소스 코드를 읽기 좋게 만드는
          것은 중요한 주제이다.

③ 샐러리 맨의 월급에서 세금을 계산하는 위의 프로그램에서 RATIO는 얼마
나 많이 사용될까? 예를 들어 200군데에서 RATIO를 사용한다고 하자. 만약
프로그래머가 매크로 상수를 사용하지 않았다면, 200군데에 0.0012345678이 나
타날 것이다.

  세금의 비율이 0.012345678로 100% 인상되었다고 하자. 프로그래머는 200군
데의 소스를 수정해야 할 것이다. 모두 고쳐진다면 문제는 덜 심각하다. 실수
로 1군데의 비율을 수정하지 못했다고 하자. 프로그래머는 알 수 없는 버그에
심각하게 고민해야 할 것이다.

  하지만, 매크로 상수를 사용하면, 프로그래머가 수정해야 할 곳은 #define을
사용해서 상수를 정의하는 오직 1곳뿐이다!

매크로 함수(macro function)

#define macro_identifier(<arg_list>) token_sequence

  위의 #define의 문법에서 두 번째 문법은 매크로 함수의 문법이다. 이것이
함수는 아니지만, 함수처럼 사용되기 때문에 이것을 매크로 함수라 한다. 하지
만, 매크로 함수 역시 매크로 확장되는 것이지, 함수의 호출은 일어나지 않는
다.

  2개의 값 중 큰 값을 구하는 MAX(a,b)를 다음과 같이 정의할 수 있다.

#define MAX(a,b) a>b?a:b

  위의 매크로 함수는 모든 MAX(a,b) 형태를 a>b?a:b 로 치환한다. 예를 들
면,

printf("%d\n",MAX(2,3));

은

printf("%d\n",2>3?2:3);

으로 치환된다. 결과는 물론 3이 출력될 것이다.

  매크로 함수는 주의해서 사용하지 않으면, 심각한 문제가 발생한다. 아래의
예를 주목하라.

#include <stdio.h>

#define MUL_DEFINED

#define MUL(a,b) a*b

void main() {

    printf("%d\n",MUL(2+3,4));//20을 예상하는가?

}

  결과는

14

이다. 대부분의 독자들은 아마도 결과로 20을 예상하였을 것이다. 결과가 왜
14인지는 매크로 확장에 의해 명확해진다.

    printf("%d\n",MUL(2+3,4));

의 매크로 확장은 2+3이 a에 해당하고, 4가 b에 해당하므로,

    printf("%d\n",2+3*4);

                  a   b

이다. 연산자의 우선 순위에 의해 결과는 14인 것이다. 매크로 함수를 정의할
때의 규칙은 다음과 같다.

"매크로 함수의 파라미터는 반드시 괄호로 묶여야 한다."

즉, MUL은 다음과 같이 정의되어야 한다.

#define MUL(a,b) (a)*(b)

사실 이것으로도 부족하다. 매크로 함수간의 우선 순위에 문제가 발생할 수도
있으므로, 표현식 전체가 괄호로 묶이는 것이 바람직하다.

#define MUL(a,b) ((a)*(b))

  위에서 정의했던, MAX() 역시

#define MAX(a,b) ((a)>(b)?(a):(b))

처럼 정의되어야 한다■.

             매크로 함수를 정의할 때, 우선 순위를 고려하는 것은 조금은 귀찮은 작
          업이다. 독자들은 C++의 주제 중 '인라인 함수(inline function)'에서 이러한
          매크로 함수의 단점을 극복하는 법을 배우게 될 것이다.

  우리가 살펴본 소스에서

#define MUL_DEFINED

라는 문장에 주목했는가? 이 문장은 MUL_DEFINED라는 상수가 단지 정의
되었다고 선언하는 것이다. 즉, 이 상수는 이미 정의되었으므로, 변수로도, 함
수 이름으로도, 다른 매크로 상수로도 재정의(redefinition)할 수 없다. 그렇다
고 MUL_DEFINED가 어떤 값을 가지는 것도 아니다.

  이러한 #define의 역할에 대해, 이어지는 절(section)에서 살펴보도록 하자.

   왜 매크로 함수를 사용하죠?

   사실 여기에 대한 대답은 진부한 것이다. "트럼펫으로도 베토벤의 소
나타를 연주할 수 있다." 하지만, 오케스트라를 사용하는 것이 좀 더 좋은 방
법이라는 것은 누구나 알고 있다. 매크로 함수는 코딩 작업을 도와주는 훌륭
한 도구이므로, 사용하지 않아도 무방하다. 하지만, 분명한 이점도 또한 존재
한다. 매크로 함수는 함수의 호출이 일어나지 않으므로, 일반 함수에 비해 빠
르다. 일반 함수는 함수의 호출 전에 기계 상태(machine state)를 저장하기 위
해, 레지스터를 스택에 저장하는 Prefix 코드와 저장된 레지스터를 복구하는
Suffix 코드를 함수의 앞뒤에 추가한다. 함수의 덩치가 조금은 커지는 것은
물론이고, 스택에 대한 오버헤드 때문에 속도도 조금은 느려지는 것이다. 하지
만, 매크로 함수는 함수 호출이 아니고, 컴파일 전에 매크로 확장되는 것이므
로, 그러한 오버헤드가 없다.

관례

  매크로 함수와 상수는 관례(convention)상 모두 대문자를 사용한다. 위의 예
들에서 우리는 모든 매크로에 대해 대문자를 사용했다. 이러한 규칙은 대부분
의 C 프로그래머들이 지키는 관례이다. 독자들도 관례를 따르기를 바란다. 이
러한 구분은 매크로와 일반 변수, 매크로 함수와 일반 함수를 구분하여 소스
코드를 읽기 좋게 한다.

   미묘한 - 하지만 중요한 - 문제

  이제 #include에서 언급했던, 미묘한 문제를 다룰 준비가 되었다. 다시 그
소스를 고려해 보자.

//a.cpp

#include <stdio.h>

#include "macro.h"

#include "b.cpp"

void main() {

    int i;

    i=f(2);

    PRINT(i);

}

//b.cpp

int f(int i) {

    return i*i*i;

}

//macro.h

#define PRINT(i) printf("%d",i)

  위의 소스에서 b.cpp에서도 macro.h가 필요해서 b.cpp를 아래와 같이 수정
했다고 하자.

//b.cpp

#include "macro.h"

int f(int i) {

    PRINT(i);

    return i*i*i;

}

  위의 프로그램이 컴파일될 것이라고 생각하는가? 그렇지 않다. 컴파일러는
매크로가 중복되었다는 에러 메시지를 출력할 것이다. a.cpp는 다음과 같이
매크로 확장된다(설명을 위해 #include만을 확장했다).

#include <stdio.h>

#define PRINT(i) printf("%d",i)

  //macro.h가 확장되었다.

#define PRINT(i) printf("%d",i)

  //b.cpp의 macro.h가 확장되었다. 이 문장에서 에러가 발생한다.

int f(int i) {

    PRINT(i);

    return i*i*i;

}

void main() {

    int i;

    i=f(2);

    PRINT(i);

}

  위의 소스에서 보듯이, b.cpp의 macro.h가 확장될 때, 이것은 분명히 에러이
다. PRINT()는 이미 정의된 매크로인 것이다.

  그러므로, 헤더 파일을 만들 때는 이것이 프로젝트에 항상 1개만이 확장된
다라는 보장이 되어야 한다. 이것을 구현하기 위해서, #if, #ifdef등을 사용한다.

  #if와 defined 연산자

  #if의 문법은 다음과 같다. 이것은 블락(block)이 섹션(section)■으로 바뀐

             섹션은 블락으로 구분되지 않은 문장들을 말한다. #if의 조건이 끝나는 문
          장(즉 매크로 확장이 끝나는 문장)은 #elif, #else 혹은 #endif까지 이다.

것을 제외하고는 if문의 문법과 같다.

    #if <expression-1>

    <section-1>

    [#elif <expression-2>

    <section-2>][...]

    [#else

    <section-3>]

    #endif

  독자들은 이미 '연산자'에서 전처리 명령문에서만 사용 가능한 연산자 #와
##에 대해서 배웠을 것이다. 전처리 명령문에서 사용 가능한 또 하나의 연산
자에

defined

가 있다. 이 연산자는 매크로 상수가 정의(definition)되어 있는지의 여부를 검
사한다. 위의 예에서 macro.h는 다음과 같이 작성되는 것이 바람직하다.

#if !defined(PRINT)

#define PRINT

#define PRINT(i) printf("%d",i)

#endif

  헤더 전체가 #if∼#endif에 의해 감싸져 있다. 이것은 여러 곳의 소스에서 이
헤더 파일을 포함시키더라도 헤더가 한 번만 포함되는 것을 허락한다. 즉 정
의가 이미 되어 있다는 것을 나타내기 위해 PRINT를 플래그(flag)로 사용한
것이다. 다시 확장된 a.cpp를 살펴보자.

//확장된 a.cpp

#include <stdio.h>

#if !defined(PRINT)//PRINT가 정의되어 있지 않으므로 아래의 두 문장은 확

                   //장된다.

#define PRINT

#define PRINT(i) printf("%d",i)

#endif

#if !defined(PRINT)//PRINT가 이미 위에서 정의되었으므로, 아래의 두문장은

                   //확장되지 않는다.

#define PRINT

#define PRINT(i) printf("%d",i)

#endif

int f(int i) {

    PRINT(i);

    return i*i*i;

}

void main() {

    int i;

    i=f(2);

    PRINT(i);

}

  실제로 #if보다는 #ifdef와 #ifndef(if not defined)가 보다 많이 사용된다.

    #if !defined(PRINT)

    #define PRINT

    ...

    #endif

는

    #ifndef PRINT

    #define PRINT

    ...

    #endif

와 동일하다. 하지만, 2가지 이상의 복합 조건(compound condition)을 검사해
야 하는 경우 #if를 사용하는 것이 효과적이다. 아래의 예에서는 PRINT가 정
의되어 있고, DEBUG가 정의되어 있지 않으면, 매크로를 확장한다.

    #if defined(PRINT) && !defined(DEBUG)

    ...

    #endif

  #if가 헤더의 중복을 피하기 위해서뿐만 아니라, 조건 컴파일을 위해서 사용
되는 경우도 많다. 예를 들어, 독자들이 프로그램을 개발할 때, 필요한 함수들
에 대해서 생각해 보자. 이러한 함수들은 프로그램을 개발하거나, 디버깅할 때
필요에 의해서 만든 함수이므로, 프로그램이 완성되어 마지막 버전을 작성할
때는 포함되지 않는 것이 바람직하다(쓸데없는 함수의 포함은 프로그램의 덩
치를 크게 한다). 이러한 함수를 f()라고 하면, 아래의 예는 #if를 사용하여 f()
를 포함하고 제거시키는 기술을 보여준다.

#include <stdio.h>

#define DEBUG

#ifdef DEBUG

void f(int i) {

    printf("%d\n",i);

}

#endif

void main() {

    int i=2,j=3,k=4;

    k=i*j*k;

    #ifdef DEBUG

    f(k);

    #endif

    printf("%d,%d,%d\n",i,j,k);

}

  위의 예에서 함수 f()의 정의와 main()에서 f()의 호출은 매크로 상수
DEBUG가 정의되어 있을 때에만 유효하다. 이 프로그램의 개발이 끝났다면,
이제 더 이상 f()는 필요 없으므로(유지 보수를 위해서 코드 자체는 남겨 두
어야 한다), 코드에서 제거시키는 것이 바람직하다. 어떤 독자들은 이러한 문
장을 주석으로 처리하여 구현할 지도 모르지만, 그것은 구식 방법이다.

  위의 예에서

#define DEBUG

문장을 단순히

//#define DEBUG

로 처리하면, 이러한 문제를 해결하게 되는 것이다. 자 이제 #if와 #define의
규칙을 외워두자.

"#if와 #define은 헤더 파일이 중복으로 포함되는 것을 방지하기 위해서,
디버깅 모드와 릴리즈 모드(release mode)의 다른 버전의 프로그램을
작성하기 위해서 사용한다."

   Visual C++ 6.0에 포함된, MFC(Microsoft Foundation Class)의 자동 코
드 생성기인 AppWizard가 자동으로 생성한 뷰 클래스(view class)의 헤더 파일
을 보자. 위에서 언급한 2개의 기교를 모두 사용하고 있다. 아래의 코드는 프
로젝트의 이름을 Test로 하여 AppWizard가 자동으로 생성한 코드의
TestView.h를 리스트한 것이다.

// TestDoc.h : interface of the CTestDoc class

//

/////////////////////////////////////////////////////////////////////////////

#if !defined(AFX_TESTDOC_H__C41CD2EC_B9DD_11D2_BC14_02608C6CA09B__INCLUDED_)

#define AFX_TESTDOC_H__C41CD2EC_B9DD_11D2_BC14_02608C6CA09B__INCLUDED_

#if _MSC_VER >= 1000

#pragma once

#endif // _MSC_VER >= 1000

class CTestDoc : public CDocument

{

protected: // create from serialization only

    CTestDoc();

    DECLARE_DYNCREATE(CTestDoc)

// Attributes

public:

// Operations

public:

// Overrides

    // ClassWizard generated virtual function overrides

    //{{AFX_VIRTUAL(CTestDoc)

    public:

    virtual BOOL OnNewDocument();

    virtual void Serialize(CArchive& ar);

    //}}AFX_VIRTUAL

// Implementation

public:

    virtual ~CTestDoc();

#ifdef _DEBUG

    virtual void AssertValid() const;

    virtual void Dump(CDumpContext& dc) const;

#endif

protected:

// Generated message map functions

protected:

    //{{AFX_MSG(CTestDoc)

        // NOTE - the ClassWizard will add and remove member functions here.

        //    DO NOT EDIT what you see in these blocks of generated code !

    //}}AFX_MSG

    DECLARE_MESSAGE_MAP()

};

/////////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}

// Microsoft Developer Studio will insert additional declarations immediately before the
previous line.

#endif // !defined(AFX_TESTDOC_H__C41CD2EC_B9DD_11D2_BC14_02608C6CA09B__INCLUDED_)

  위의 소스에서 #if∼#endif가 사용된 기교에 대해서만 주목하라. 다른 것은
보지 말라.

   #undef, #line, #error 와 #pragma

  #undef는 이미 정의된 매크로를 해제한다. 예를 들어 아래의 문장은 에러가
아니다.

#define PRINT(i) printf("%d\n",i)

#undef PRINT

#define PRINT(i,j) printf("%d,%d\n",i,j)

  위의 소스에서는 이미 정의된 PRINT를 재정의(redefinition)하기 위해
#undef를 사용하고 있다.

미리 정의된 매크로(predefined macros)

  컴파일러 벤더(vendor)는 컴파일러 에러 메시지 출력 등을 위해 몇 개의 매
크로를 미리 정의하여 두었다. 아래의 매크로는 Borland C++ 4.5의 도움말에
서 참고한 몇 개의 매크로이다. 독자들도 자신이 사용하는 컴파일러의 도움말
을 참고하여 보라.

    __cplusplus

    __BCPLUSPLUS__

    __FILE__

    __LINE__

  __cplusplus는 표준 매크로이므로, 어떤 컴파일러에서도 사용할 수 있다. 이
매크로는 소스가 C++모드로 컴파일되고 있음을 지시한다. 이 매크로는 일반
적으로 C와 C++의 혼합 프로그래밍을 위해 사용된다.

#ifdef __cplusplus

extern "C" {

#endif

void f();

int g(int);

void h(int,int);

#ifdef __cplusplus

}

#endif

  이미 C로 컴파일된 함수가 있다면, 이 함수를 바로 C++에서 사용하기 위해
서는 이러한 함수가 C에서 컴파일되었다고 알려 주어야 한다. 이것은 C++의
함수와 C의 함수가 호출되는 방식이 조금 다르기 때문이다(오버로딩등을 위
해 C++의 함수는 이름 장식(name decoration)을 한다).

  그래서 이미 컴파일되어 .OBJ 혹은 .LIB로 존재하는 함수의 헤더를 C++ 소
스에서 포함할 때, 이러한 C 함수의 헤더는

extern "C" {}

블락안에 선언되어야 한다. 위의 예는 현재의 컴파일 모드■가 C++일때, C로

             일반적으로, 확장자가 CPP이면, C++모드로 컴파일된다. 확장자가 CPP가
          아니면, C모드로 컴파일된다.

만든 함수, f(),g()와 h()를 포함하는 기교를 보여준다.

  미리 정의된 매크로는 관례상, 앞과 뒤에 모두 2개의 언더스코어
(underscore)로 감싸 있다. __BCPLUSPLUS__는 컴파일러가 Borland C++ 컴
파일러인 경우 정의되어 있다.

  볼런드의 mem.h에 해당하는 Visual C++의 헤더는 memory.h이다. 아래의
코드는 이 매크로를 사용하여, Borland C++와 Visual C++ 모두에서 컴파일되
는 소스를 작성하는 것을 도와준다.

    #ifdef __BCPLUSPLUS__

        #include <mem.h>

    #else

        #include <memory.h>

    #endif

  __FILE__과 __LINE__은 각각 현재 소스 파일의 이름과 줄 번호를 출력하
는 매크로 상수이다■.

             이러한 매크로는 한 번 정의되어 계속해서 같은 값으로 사용될 것이라고
          생각해서는 안된다. 이것은 분명 매크로이지만, 컴파일 과정동안 컴파일러
          에 의해서 변경된다.

  아래의 예를 참고하라(소스 파일을 work.cpp로 저장하였다).

#include <stdio.h>

void main() {

    int i=2,j=3,k=4;

    printf("%d,%d,%d\n",i,j,k);

    printf("%d\n",__LINE__);

    printf("%s\n",__FILE__);

}

  출력 결과는

    2,3,4

    7

    WORK.CPP

이다. __LINE__은 현재의 줄 번호를 십진 상수로 정의한다. 위의 예에서 결과
가 7인 이유는 printf("%d\n",__LINE__); 가 work.cpp의 7번째 줄이기 때문이
다.

  __LINE__이 비록 값이 갱신되기는 하지만, 이것은 변수가 아니라, 사용자가
재 정의할 수 없는 매크로 상수(?)이다■.

             사용자가 __FILE__ 혹은 __LINE__을 재정의 할 수 없는 것은 당연하다.
          실제 이것은 매크로 상수라기 보다는, 컴파일러가 컴파일 과정동안 내부적
          으로 유지하는, 컴파일러 변수(compiler variable)이다.

  #line은 매크로 상수 __LINE__과 __FILE__을 재설정하기 위해 사용한다.
문법은 다음과 같다.

#line integer_constant ["filename"]

예를 들면,

#line 10 "hello.cpp"

는 #line이후부터 __LINE__을 10부터 카운트하며, __FILE__을 hello.cpp로 설
정한다. 아래의 예를 참고하라.

#include <stdio.h>

void main() {

#line 1 "main.cpp"

    int i=2,j=3,k=4;

    printf("%d,%d,%d\n",i,j,k);

    printf("%s, %d\n",__FILE__,__LINE__);

    printf("%d\n",__LINE__);

#line 3897

    printf("%s, %d\n",__FILE__,__LINE__);

}

출력 결과는

    2,3,4

    main.cpp, 4

    5

    main.cpp, 3897

이다.

  #error는 컴파일 과정동안 에러 메시지를 출력하기 위해 사용한다. 아래의
예를 참고하라.

#include <stdio.h>

#define TYPE 1

void main() {

    int i=2,j=3;

    #if (TYPE!=0)

    #error You must define TYPE to 0

    #endif

    printf("%d,%d\n",i,j);

}

  위의 소스는 컴파일되지 않는다. 컴파일 과정 중에 다음과 같은 컴파일 에
러 메시지가 출력된다.

Error directive: You must define TYPE to 0 in function main()

  실제로 #error와 #pragma는 거의 사용하지 않는다. 만약 여러분이 만든 함
수가 오로지 C++모드에서만 컴파일되기를 원한다면, 다음과 같은 코드를 프
로그램의 처음에 추가할 수도 있다.

#ifndef __cplusplus

#error You must compile this in C++ mode

#endif

  실제로 위와 같은 경우를 제외하고 #error는 사용할 일이 없을 것이다.

운영체제나 환경에 의존적인 설정이 필요하다면?

  만약 컴파일러 설계자에 의해 설정되어야 하는 값들이 표준 매크로로 나타
내기 불가능한 것이라면, 어떻게 이러한 것을 처리할 것인가?

  C++ 표준 위원회는 이러한 값들을 위해 #pragma를 추가하였다. #pragma
는 실용주의(pragmatism)를 표방하는(?) 전처리 명령문이다.

  예를 들면, 볼런드(InpriseTM) 계열의 컴파일러에서

#pragma inline

은 소스 코드가 인라인 어셈블러(inline assembler)를 포함한다는 것을 나타낸
다. #pragma는 컴파일러에 의존적이므로, 위의 소스가 Visual C++에서도 같
은 의미를 가진다는 보장은 없다■.

             하지만, Visual C++에서도 같은 의미이다. 하지만, 이것이 표준은 아니다.

  실습문제

1.


21. 가변 인자

21. 가변 인자(variable
argument)

  printf()는 가변 인자 리스트(variable argument list)를 사용하는 C의 표
준 함수이다. 비록 C++에서는 가변 인자 없이도, 연산자 오버로딩에 의해
cout에서 가변 개수의 인자를 사용하는 것이 가능하지만, C++에서도 가변 인
자는 여전히 중요한 주제이다.

  가변 인자 함수를 작성하기 위해서는 생략 심벌(ellipse symbol: ...)과 형
va_list와 3개의 매크로 va_start, va_arg와 va_end를 사용한다. 형 정의와
매크로는 stdarg.h에 포함되어 있으므로, 반드시 stdarg.h를 포함(include)해야
한다.

  가변 인자를 사용하는 함수 Sum()을 만들어 보자. 함수 Sum()의 첫 번째
파라미터는 자신을 제외한 가변인자의 수이며, 나머지는 정수 가변 인자이다.
Sum()은 정수 가변 인자의 합을 리턴한다. Sum()은 다음과 같은 프로토타입
(prototype)을 가진다.

int Sum(int n, ...);

  프로토타입에 사용된 심벌 ... 에 주목하라. 이것은 Sum()함수가 가변 인자
를 파라미터로 가짐을 명시한다. 예를 들어 Sum(3,5,6,7); 의 결과는 18이다.
Sum(10,1,2,3,4,5,6,7,8,9,10); 의 결과는 55이다.

  이제 Sum()의 몸체(body)를 작성한다. 먼저 va_list형의 변수를 한 개 선언
해야 한다(일반적으로 ap로 선언한다). 그리고 합을 저장할 변수 s를 0으로
초기화시키고, 인덱스 변수 i와 임시 변수 t를 선언한다.

int Sum(int n,...) {

    int s=0,i,t;

    va_list ap;

}//Sum

  다음에 va_start 매크로를 사용하여 va_list형의 변수 ap와 Sum()함수의 첫
번째 파라미터를 지정한다. 이것은 ap가 Sum()의 두 번째 인자의 시작 주소
를 가리키게 한다.

int Sum(int n,...) {

    int s=0,i,t;

    va_list ap;

    va_start(ap,n);//이 문장은 ap가 n이후의 첫 인자의 시작 주소를 가리키

                   //게 한다.

}//Sum

  va_start 매크로 이후에 원하는 형의 변수 값을 읽기 위해 va_arg매크로를
ap와 함께 사용한다. 예를 들어 정수 값을 읽고 싶으면 va_arg(ap,int); 실수
값을 읽고 싶으면 va_arg(ap,float); 를 사용한다. 이것은 실제 ap가 두 번째에
지정된 형(type)만큼을 읽고 ap포인터를 sizeof(type)만큼 증가시키는 일을 한
다. 이 예에서는 정수가 n개 지정되어 있으므로 아래와 같이 작성한다.

int Sum(int n,...) {

    int s=0,i,t;

    va_list ap;

    va_start(ap,n);

    for (i=0;i<n;++i) {

        t=va_arg(ap,int);

        s+=t;

    }//for

}//Sum

  인자를 꺼내는 작업이 끝났으면, va_end 매크로를 사용하여 끝임을 알리고,
나머지 작업을 한다. va_end는 실제 아무 일도 하지 않는다. va_start와 쌍을
맞추기 위해 사용한다. 완성된 함수는 아래와 같다.

#include <stdarg.h>

...

int Sum(int n,...) {

    int s=0,i,t;

    va_list ap;

    va_start(ap,n);

    for (i=0;i<n;++i) {

        t=va_arg(ap,int);

        s+=t;

    }//for

    va_end(ap);

    return s;

}//Sum

  이 프로그램의 예를 살펴보자.

cout << Sum(3,5,6,7) << endl;

의 출력 결과는

18

이다.

cout << Sum(2,5,6,7) << endl;

의 출력 결과는 얼마일까? 비록 3개의 인자를 더해야 함에도 불구하고 첫 번
째 파라미터가 2이므로 결과는

11

이다. 자 마지막으로 다음의 결과는 얼마일까?

cout << Sum(3,5L,6,7) << endl;

결과는

11

이다. 두 번째 파라미터의 형이 long임에 주목하라. Sum()의 내부에서
va_arg(ap,int)를 사용하여 정수(integer)만을 꺼내므로, 결과는 5+0+6이 되어
11이 된 것이다■. 이것의 자세한 동작은 스택을 이해해야 한다.

             이것은 printf("%d,%d\n",3L,4L); 의 결과가 3,4가 아니라, 3,0인 것과 같다.
          printf()는 가변 인자의 형을 판단하기 위해 첫 번째 문자열의 이스케이프
          절차(%d등)를 이용한다.

#include <iostream.h>

#include <stdarg.h>

#include <stdio.h>

int Sum(int n,...) {

    int s=0,i,t;

    va_list ap;

    va_start(ap,n);

    for (i=0;i<n;++i) {

        t=va_arg(ap,int);

        s+=t;

    }//for

    va_end(ap);

    return s;

}//Sum

void main() {

    cout << Sum(3,5L,6,7) << endl;

    printf("%d,%d\n",3L,4L);

}

  위 프로그램의 출력 결과는

    11

    3,0

이다.

  스택 동작

  독자들은 먼저 C의 스택 동작을 이해해야 한다. 이것은 지역 변수(local
variable)가 스택에 할당되기 때문이다. 함수의 인자는 지역 변수이므로 스택
에 푸시(push)된다. 예를 통하여 C의 스택 동작을 다시 한번 살펴보자. 먼저
함수 호출에 관한 다음 사항을 상기하기 바란다.

"C의 함수 호출은 C방식(키워드 cdecl사용)을 따른다. C방식은 인자를
스택에 푸시하는 일을 불리는 쪽(callee)이 담당한다. 또한 오른쪽에서
왼쪽으로 스택에 푸시한다."

  그러면 다음 함수를 호출했을 때, 스택의 상태를 그려보자. 이 함수는 문자
형, 정수형, 긴 정수형 인자를 가진다.

int Add(char c, int i, long l);

  스택의 상태는 아래 그림과 같다.

             스택은 상위 주소에서 하위 주소로 자란다. 그러므로 먼저 l이 5003~5006
          번지에 할당되고, i가 5001~5002번지에 할당되고, 마지막으로 c가 5000번지
          에 할당된다. 이 때 스택 포인터는 4999를 가리키고 있을 것이다. 이 때
          어떤 포인터 변수 ap가 i를 가리키도록 하기 위해서는 어떤 방법을 사용해
          야 하는가?

  여기서 우리의 목적은 인자의 직접적인 참조를 사용하지 않고 i와 l을 참조
하려는 것이다. 이것은 c의 주소를 알면 가능하다. 포인터 변수 ap가 c를 가리
키도록 한 다음(ap=5000), c의 크기 sizeof(c)만큼을 더 하면, ap+=sizeof(c) ap
는 5001을 가리킨다. 즉 정수 i를 가리킨다. 다시 ap가 l을 가리키도록 하려면,
i의 크기만큼을 더하면 될 것이다.

ap+=sizeof(i)

  이제는 ap가 5003이 되어 l을 가리키게 된다.

             ap는 i(5001)을 가리킨다. 이것은 5000에 sizeof(c)를 더한 결과이다. ap2는
          l(5003)을 가리킨다. 이것은 ap에 sizeof(i)를 더한 결과이다.

  1개의 형과 3개의 매크로

  va_list, va_start, va_arg와 va_end의 소스(stdarg.h에 있슴)는 아래와 같다.

typedef void _FAR *va_list;

#define __size(x) ((sizeof(x)+sizeof(int)-1) & ~(sizeof(int)-1))

#define va_start(ap, parmN) ((void)((ap) = (va_list)((char _FAR \

    *)(&parmN)+__size(parmN))))

#define va_arg(ap, type) (*(type _FAR *)(((*(char _FAR *_FAR \

    *)&(ap))+=__size(type))-(__size(type))))

#define va_end(ap)          ((void)0)

  va_list는 단순히 원거리 포인터(far pointer)이다. va_start(ap,parmN)은
va_list형의 ap가 parmN의 다음 주소를 가리키게 한다. 즉 ap는 두 번째 인자
의 시작 주소를 가리키게 된다.

  va_arg(ap,type)은 ap가 가리키는 곳에서 type을 꺼내고 ap를 type의 크기
만큼 증가시킨다. va_end(ap)는 아무 일도 하지 않는다.

  이제 위의 예에서

cout << Sum(3,5L,6,7) << endl;

의 결과가 11이 찍힌 과정을 그림으로 그려보자.

             va_start(ap,n)은 ap가 [5000] 즉 3L의 상위 바이트를 가리키도록 한다.
          va_arg(ap,int)는 [5000]~[5001]의 5를 리턴하고 ap를 2(sizeof(int)) 증가시킨다.

             두 번째 va_arg(ap,int) 호출은 0을 리턴한다. ap는 2증가되어 [5004]를 가리
          킨다.

             3번째 va_arg(ap,int)호출은 6을 리턴한다. ap는 [5006]으로 설정된다.

  위의 그림에서 살펴보듯이 s에는 5+0+6이 저장된다.

  5L이 저장될 때 어떤 PC는 하위 바이트가 상위에 저장되기도 하지만, IBM
호환 PC의 경우는 하위 바이트는 하위 번지에 상위 바이트는 상위 번지에 저
장된다. 5L에서 5를 이루는 부분은 하위 바이트이므로 [5002]보다 하위 번지
인 [5000]에 저장된다. IBM PC의 이러한 독특한 숫자 저장 방식을 역워드 형
식이라 한다.

다른 예(another example)

  문맥 자유 문법(context free grammar)을 처리하기 위해 문법을 2차원 문자
배열에 저장하는 함수를 설계해 보자. 소스는 아래와 같다.

#include <string.h>

#include <stdarg.h>

#define MAX_GRAMMAR_NUM         30

#define MAX_GRAMMAR_LEN         20

int nGrammar;//number of valid grammars

char strGrammar[MAX_GRAMMAR_NUM][MAX_GRAMMAR_LEN];

void SetGrammar(int n, ...) {

    va_list ap;

    char* arg;

    int i;

    nGrammar=n;

    va_start(ap,n);

    for (i=0;i<nGrammar;++i) {

        arg=va_arg(ap,char*);

        strcpy(strGrammar[i],arg);

    }//for

    va_end(ap);

}//SetGrammar

  전역 변수 strGrammar[]는 각각의 문자열을 유지하며, 이를 위해 strcpy()
함수를 사용하고 있다. 스택에는 문자열의 시작 주소가 들어 있으므로,
va_arg(ap,char*) 를 사용하여 차례대로 문자열을 꺼내고 있다.

  main()에서는 다음과 같이 문법 문자열을 설정할 수 있다.

...

void main() {

    SetGrammar(9,"1->23",

                 "2->2",

                 "3->24",

                 "4->25",

                 "5->65",

                 "6->7",

                 "7->8",

                 "8->9[3]",

                 "9->9");

}//main

  실제로 위의 소스는 'L-system'에서 사용된 것이다.

  실습문제

1. printf()의 이스케이프 절차(escape sequence) %d, %f, %ld등이 가변인자
리스트로 어떻게 동작하는지 설명하라(힌트: printf("%d,%d",0x12L);의 출력결
과는 얼마인가?).

2. printf()와 똑같이 동작하는 MyPrintf()를 작성하라.

3. printf()외에 가변인자 리스트를 사용하는 모든 표준 함수에 대해 설명하라.


22. 메모리(memory)

  이 장에서는 메모리에 대해서 포괄적인 주제를 다룬다. 이 장에서 다룰 내
용은 다음과 같다.

  ① delete 와 delete[]의 차이점은?

  ② 왜 Borland C++ 3.1에서는 주소가 xxxx:xxxx 형태로 찍히는가?

  ③ 가상 메모리(virtual memory), 스왑 파일(swap file)

  ④ 선형 주소(linear address), 세그멘테이션(segmentation)

  ⑤ 64K의 한계, 4Giga의 한계(?)

  ⑥ 단편화(fragmentation), 컴팩션(compaction)

   옛날 옛적에...

  필자가 처음 컴퓨터를 접한 것은 SPC-1000이라고 불리는 8비트 컴퓨터였
다. 그 당시 난 중학생이었는데, 처음 컴퓨터란 물건을 내게 알게 해 준 작은
삼촌께 이 자리를 빌어 감사의 말을 전하고 싶다.

  그 당시 그 컴퓨터에는 아마도 32K의 메모리(최대 64K까지 장착 가능)가
장착되어 있었을 것이다. 물론 하드 디스크(hard disk), 플라피 디스크(floppy
disk)는 없었고, 테이프(tape)에서 게임을 읽어서 오델로(othello) 등의 게임을
하던 생각이 난다. 그 컴퓨터는 부팅시키면 바로 (아마도) GW-BASIC이 실
행되었는데, 따로 운영 체제는 존재하지 않았던 것 같다. 롬(ROM)의 BASIC
이 실행되었던 것이리라...

  약 5년 뒤 난 컴퓨터공학과에 입학한 후(1988년), IBM호환 기종 PC
XT(eXtended Technique)를 처음 구입하게 되었다. 비록 하드 디스크는 없었
지만, 메모리가 1Mega나 되는 거대한 컴퓨터였다.

  그 당시 소련에서 만든 테트리스(tetris)가 굉장한 인기였는데, 우리 나라 최
초의 바이러스인 C-Brain바이러스가 처음 등장한 해이기도 하다. 이 바이러스
를 치료하는 프로그램인 백신(vaccine)을 만든 사람은 지금 꾀 저명한 인물이
되었고, 입학 동기였던 누구는 '큰사람'이라는 벤처를 창업해서 국산 소프트웨
어의 발전을 위해 열심히 일하고 있다.

  필자도 한 때, '꼬마'라는 허큘리스 그래픽 소프트웨어를 개발해서 통신망에
공개한 적이 있는데, 90년대쯤에 대학을 다닌 사람은 아마 기억하는 사람이
있을지도 모른다.

  8bit CPU를 사용했던 PC가 어떻게 주소를 64K까지 지원할 수 있었을까?
16bit 인텔8088■을 사용했던, PC가 또한 어떻게 주소를 1Mega까지 지원할

             물론 인텔의 첫 번째 16bit 마이크로 CPU는 i8086이었다. 하지만, 이 제품
          은 높은 단가로 시장에서 실패했다. 그 때까지 시장을 주름잡던 8bit
          AppleII를 따돌리기 위한 IBM의 작전은 실패로 끝나는 듯 했다. 가격이 상
          승된 이유는 PC를 구성하는 모든 칩 및 부품들이 16bit를 위해, 새로 제조
          되어야 했기 때문이었다. 그래서 IBM은 인텔에게 CPU는 16비트로 동작하
          면서, 외부 장치와는 버스(bus)를 통해 8bit로 동작하도록 CPU를 설계할 것
          을 요청했다. 486시절까지 이것은 SX로 불렸지만, 그 당시 이 새로운 CPU
          의 이름은 i8088이었다. 이것은 대 성공이었다! 가격은 낮아졌고, 성능은 높
          아졌으면, 새로운 운영체제 MS-DOS 1.0은 그 당시의 CP/M과 비슷했다.
          하지만, 이것은 후에 IBM의 성공이 아니라, Intel 아니, Microsoft의 성공이
          었다. 후에 IBM의 새로운 운영체제인 OS/2, GNU의 Linux가 나왔지만,
          Microsoft의 Windows를 따라잡지 못했고, Apple의 Machitosh가 Pentium을
          사용하는 IBM 호환 PC를 따라잡지는 못했다.

수 있었을까? 이러한 주소 지원은 i8086을 설계했던 인텔 기술 팀이 해결해야
했던, 첫 번째 과제였다.

   어려운 선택, 세그멘테이션(segmentation)

  8비트 Apple은 주소 지정을 위해 8비트 레지스터를 2개 사용⼆했다. 비록

             이것은 피할 수 없는 선택이었다. 주소 지정을 위해 8비트만을 사용한다
          면, 메모리는 겨우 28=256Byte밖에 지정할 수 없다.

내부적인 데이터의 처리 및 흐름은 8비트였지만, 주소 지정을 위해 16비트를
사용했던 것이다⼆. 물론 16비트 주소를 내보내기 위해 8비트의 주소를 2번 내

             지금은 사라졌지만, 486시절까지 이러한 방식으로 동작하던 CPU를 SX라
          고 불렀으며, 그렇지 아니한 CPU를 DX라고 불렀다. 예를 들면, 386SX는
          내부적으로는 32비트로 동작했지만, 주소 생성 및 데이터의 전달을 위해
          16비트 버스를 사용했다. 이것은 AT를 386으로 Upgrade할 수 있었던 이유
          이다.

어 보냈다.

             8비트 CPU에서 16비트 주소의 사용: 16비트 주소를 생성하기 위해, 2개의
          8비트 주소 레지스터를 사용했다. 8비트 Apple 시절 최대 주소는 216=64K
          였다. 외부와 연결된 8비트 버스를 통해 8비트의 데이터가 2번 전송되었
          다.

  그러므로, 주소의 범위는 0∼216(26×210=64K) 였다. 이것은 Apple에 장착 가
능한 메모리의 크기가 64K가 된 이유였다.

인텔의 선택: 세그멘테이션(segmentation)

  인텔은 메모리의 크기를 두고 고민했다. 비록 Apple의 CPU가 사용한 아키
텍쳐(architecture)를 사용할 수 있었음에도 불구하고, 그것은 4Giga(약 4×
109)라는 거대한 주소를 지원했고⼆, 이러한 굉장한 크기의 주소는 PC에서는

             16비트 레지스터를 2개 병렬로 연결하는 아키텍쳐를 그대로 사용하면, 22
          ×230=4Giga Byte의 메모리를 지원하는 것이 가능했다. 인텔의 기술자들은
          아마도 이렇게 생각했을 것이다. 'Wow! It's too large, It will be never
          used!' 인텔은 결국 세그멘테이션(segmentation)을 선택했고, 이것은 Win32
          가 나타나기전까지 인텔 CPU에서 프로그램하는 프로그래머들의 골칫거리
          였다.

절대로 사용할 일이 없을 거라고 인텔의 기술자들은 생각했다. 이것은 인텔의
결정적인 실수였다. 지금은 대부분의 사람들이 16Mega 이상의 메모리를 사용
하고 있는 것이다. 필자는 몇 일전(지금은 1999년 3월 15일) PC를 Pentium II
350, 64Mega에 하드 디스크의 용량은 8Giga로 업그레이드(upgrade)했다. 64K
를 지원하는 PC가 나온지, 약 20여 년만에 일반적인 메인 메모리의 요구량은
×1000배, 하드 디스크는 약 ×100,000배 증가한 것이다!

  인텔은 널리 알려진 세그멘테이션(segmentation)이라는 주소 지정 기법
(addressing mode)을 자사의 i8086에 도입했다. 이것은 메모리를 세그먼트
(segment)라고 불리는 몇 개의 블락(block)으로 나눈 다음, 주소를 지정하기
위해,

블락 번호(block number)+블락의 처음에서 상대 주소(offset)

를 사용하는 방식이었다.

  인텔은 16개의 세그먼트 블락을 사용했고, 상대 주소 지정을 위해 16비트를
사용했다. 그러므로, 1개 블락의 크기는 216=64K였다. 이러한 64K 크기의 블
락을 세그먼트라고 한다. 이러한 세그먼트가 모두 16개있으므로, 인텔 i8086이
지원하는 주소 공간의 크기는

24×216=220=1Mega

이다.

  어떻게 20비트의 주소를 생성하는가? 이것은 16비트 레지스터를 2개 겹치
게 배열함으로써 가능하다. 아래의 그림을 보자.

             세그멘테이션의 주소 지정(i8086의 경우): 20비트의 주소는 16비트 레지스
          터에 의해 2곳에 나뉘어 저장되었다. 실제로 메모리의 한 곳을 지정하는
          주소는 첫 번째 레지스터인 베이스(base) 레지스터가 세그먼트 블락의 시
          작을 지정하면, 두 번째 레지스터인 오프셋(offset) 레지스터가 세그먼트 블
          락의 처음에서 오프셋을 지정했다.

  세그먼트 블락의 시작을 가리키는 16비트 레지스터를 베이스 레지스터
(base register)라고 하고, 블락의 처음에서 상대 주소를 가리키는 16비트 레
지스터를 오프셋 레지스터(offset register)라고 하자. 베이스의 하위 12비트
와 오프셋의 상위 12비트는 겹치게 배열되었다. 엄격하게 말하면, 겹치는 12비
트의 값은 수학적으로 더한(add) 결과가 되었다.

  예를 들면, 베이스 값이

0100 0000 0011 0111

이라고 하고, 오프셋의 값이

0000 0000 0001 0001

이라고 하자. 이것은 1Mega 메모리에서 세그먼트 블락이

0100 0000 0011 0111 0000

에서 시작하고(끝에 4비트가 추가된), 이 세그먼트 블락에서

0000 0000 0001 0001

만큼 떨어진 메모리 공간을 의미한다. 아래 그림을 보자.

             20비트의 계산: 20비트 주소를 계산하기 위해, base의 하위 4비트가 0으로
          채워진 20비트 세그먼트 블락의 시작 주소와, 16비트 offset의 값을 더한다.
          실제의 주소는 0100 0000 0011 0111:0000 0000 0001 0001(base:offset)임에
          도 불구하고, 사용자는 20비트 값인 0100 0000 0011 1000 0001이라고 생각
          한다.

  하위 4비트가 0으로 채워진 20비트의 base값은 1Mega 메모리 상에서 세그
먼트 블락의 시작을 가리킨다. 실제로 20비트의 주소는 존재하지 않는다는 것
을 주의하라. 하위 4비트는 항상 0이므로, 실제 20비트가 가리키는 값은 상위
16비트만이 사용된다. 즉, 24=16Byte으로 나뉘어진 작은 블락(small block)의
시작만을 가리킬 수 있다. 이 때 0에서 시작하는 24단위의 작은 블락을 패러
그래프(paragraph)라 한다. 그래서 베이스 값은 패러그래프의 경계만을 가리
킬 수 있는 것⼆이다.

             패러그래프의 경계는 가끔 미묘한 문제를 일으킨다. 필자는 DOS용 게임
          엔진에서 더블 버퍼링(double buffering)을 구현하면서, 패러그래프의 경계
          문제에 접해서 매우 고생한 적이 있다.

  이렇게 64K 블락의 시작이 정해지면, 16비트 오프셋 레지스터가 블락의 시
작에서 상대 주소(relative address)를 가리킨다.

  하지만, 사용자는 이러한 2개의 16비트 주소 레지스터가 더해진 20비트 주
소를 메모리 주소로 간주하는 것이다. 위의 그림에서 보면, 실제의 주소는 2개
의 16비트 레지스터에 유지되지만, 사용자에게는 더해진 결과인 20비트가 보
여진다.

  이러한 주소 지정은 서로 다른 베이스와 오프셋을 가지는 2개의 서로 달라
보이는 주소가 실제로는 같은 곳을 지정하는 문제가 발생한다.

  예를 들면, 20비트 주소

0100 0000 0011 1000 0001

는

0100 0000 0011 0111:0000 0000 0001 0001

에 의해서도 혹은

0100 0000 0011 1000:0000 0000 0000 0001

에 의해서도 생성된다. 이것은 실제 같은 곳을 가리키지만, 포인터 비교
(pointer comparison)에서는 다른 곳(포인터는 32비트로 유지된다)을 가리킨다
고 오해될 수도 있다. 그래서 이러한 포인터를 일관된 표현 방법으로 고치는
것을 포인터의 정규화(normalization)라 한다. 그러므로, 세그멘테이션에서
포인터가 정규화되면, 메모리의 한 곳을 가리키는 포인터는 항상 같은 방식으
로 표현된다. 하지만, 정규화 때문에 속도는 조금 느려질 것이다.

  사용자가 C++로 작성한 소스 코드(source code)는 기계어로 번역될 때, 코
드 영역(code area)과 데이터 영역(data area)이 구분되어져서 번역된다. 그
리고 세그멘테이션에서 코드 영역이 저장되는 세그먼트 블락을 코드 세그먼
트(code segment), 데이터 영역이 저장되는 세그먼트 블락을 데이터 세그먼
트(data segment)라 한다.

   64K의 한계

  배열은 컴파일러가 어떻게 구현할까? 배열의 요소(element)를 참조하기 위
해 인덱스(index)를 사용하는 사실에 주목하라. 배열은 연속된 메모리 블락에
할당된 다음, 인덱스를 이용한 상대 주소로 요소를 참조하도록 코드가 생성된
다. 즉 배열의 시작 주소가 베이스로 고정되고, 인덱스가 오프셋으로 표현되는
것이다. 예를 들어,

int a[5];

처럼 배열을 선언했다면, 배열 a는 10바이트의 연속된 메모리 공간에 할당된
다. 배열이 할당된 곳의 시작 주소, 즉 a를 [1000]이라 하자. 사용자가 3번째
요소를 참조하기 위하여

a[2]

처럼 사용하면, a에서 2만큼 떨어진 정수(16비트 운영체제에서 정수는 2바이
트이다) 즉 [1000]+2*2로 해석하는 것이다.

  아래의 예를 보자.

#include <stdio.h>

int a[5]={1,2,3,4,5};

int* ap=a;

void main() {

    printf("%d,%d\n",a[2],*(ap+2));

}

  결과는

3,3

이다. 즉, a[2]는 'a의 주소+4'로 해석하는 것이다. 이것은 컴파일러가 배열의
요소를 실제로 참조하는 방법이다.

  배열의 전체 크기가 64K를 넘어가면 어떠한 문제가 발생할까? 이제 배열의
요소를 참조하기 위해, 위와 같은 방법으로 구현하는 것은 불가능하다. 배열의
시작 주소에서 64K의 경계를 넘는 순간, 배열의 시작 주소는 바뀌어야 한다.
이러한 구현이 불가능한 것은 아님에도 불구하고, 볼런드(borland) 등 대부분
의 컴파일러 벤더(vendor)들은 배열의 크기를 64K로 제한했다. 이러한 제한을
64K의 한계라 한다. 아래의 프로그램에서 무엇이 잘못일까?

#include <stdio.h>

char a[65536]={0,};//64K의 한계를 무시한 에러!

void main() {

    printf("%c\n",a[0]);

}

  위의 프로그램에서 문법적인 에러(syntax error)는 없다. 하지만, 16비트 운
영체제의 64K의 한계를 무시한 논리적 에러가 발생한다. 에러 메시지는

"Array size too large"

이다. 물론 위의 프로그램을 세그멘테이션을 사용하지 않는 32비트 운영체제
의 32비트 컴파일러에서 작성한다면, 에러가 아니다.

메모리 모델(memory model)

  코드 및 데이터는 몇 개의 세그먼트 블락을 사용할까? 코드와 데이터 세그
먼트가 각각 몇 개의 세그먼트 블락을 사용하는가에 따라서, 포인터를 표현하
는 방식은 분명 달라져야 한다. 만약 데이터가 오로지 1곳의 세그먼트에 저장
된다면, 데이터를 가리키는 포인터중 베이스 값은 고정되어 있으므로, 프로그
램이 종료할 때까지 바뀌지 않는다. 그러므로 포인터를 위해 오로지 오프셋
값인 16비트만 유지하면 될 것이다.

  하지만, 데이터가 2곳의 세그먼트에 저장된다면, 베이스 값도 바뀌어야 하므
로, 포인터는 32비트로 표현되어야 할 것이다.

  이처럼 코드와 데이터가 차지하는 세그먼트의 수에 따라, 메모리 모델이 나
뉜다. 그리고 각각의 표준 함수는 각각의 메모리 모델에 따라 모두 존재한다.
볼런드 C++ 3.1에서 이 사실을 확인해 보자. 먼저 아래의 소스를 BC++ 3.1에
서 작성해 보자.

#include <stdio.h>

int i=1;

void main() {

    char* s="hello";

    printf("%p,%p\n",&i,s);

}

  Option->Compiler->Code generation...을 선택하여 메모리 모델로 Small을
선택한다.

             Code generation...: 여기서에 메모리 모델 및 최적화 방식 등을 지정할 수
          있다.

  나타나는 대화상자에서 메모리 모델로 Small을 선택한다.

             Small 모델: 이 모델은 코드와 데이터 각각 1개의 세그먼트를 차지한다.

  Compiler->Build All을 선택하여 실행 파일을 만들어 보자.

             CS.LIB 라이브러리의 선택: Small 모델로 컴파일하면 CS.LIB가 선택되어
          실행 파일이 만들어진다.

  실행 결과는

00AA,00Ac

이다. 코드와 데이터 각각이 1개의 세그먼트만을 사용하므로, 주소를 찍으면
베이스 값은 찍히지 않고, 오프셋 값만 찍힌다. 하지만, 베이스가 내부적으로
는 유지된다는 것을 주의하라. 이러한 16비트 포인터를 near 포인터라 한다.
즉, near 포인터는 베이스 값이 고정된(fixed) 포인터이다.

  이제 메모리 모델을 Large로 바꾸어 보자. Large 모델은 코드와 데이터 모
두 여러 개의 세그먼트를 사용하는 모델이다. 그러므로, 포인터는 20비트를 표
현하기 위해, 32비트로 유지해야 한다⼆.

             DOS에서 주소는 20비트이지만, 내부적으로 항상 32비트로 유지된다. 그
          러므로, 컴파일러가 포인터를 메모리에 할달할 때 32비트로 할당해야 한
          다. 하지만, Small 모델에서 코드 세그먼트의 베이스 값과, 데이터 세그먼
          트의 베이스 값은 고정되어 있으므로, 포인터는 16비트로 할당한다.

  Large 모델의 결과는

231F:0094, 231F:0096

이다. Large 모델에서 포인터는 '베이스:오프셋' 형태로 표현된다.

  선형 주소(linear address)

  그렇다면 Windows™같은 32비트 운영체제에서는 주소를 어떻게 표현할까?
Windows에서는 주소를 나타내기 위해, 더 이상 세그멘테이션을 사용하지 않
는다. 주소는 선형적인 32비트로 표현된다. 이러한 주소를 선형 주소(linear
address)라 한다. 그러므로 더 이상 메모리 모델도 존재하지 않는다.
Windows의 Win32는 인텔 CPU의 프로그래머에게 마침내 64K의 한계에서
벗어나는 길을 마련해준 것이다.

             Visual C++ 6.0: 이 컴파일러는 32비트 운영체제인 Windows에서 동작하는
          32비트 전용 컴파일러이다. 그러므로, 더 이상 64K의 한계는 존재하지 않
          는다.

  위의 프로그램을 Win98의 Visual C++ 6.0에서 컴파일하면, 결과는

00412A30,00412A3C

이다. 정확하게 8자리의 16진수, 즉 32비트의 주소로 표현되는 것이다.

   동적 할당, 그 내부(internal)

  자 아래의 프로그램에서 이상한 부분을 찾아보자. 물론 문법 에러(syntax
error) 혹은 논리적 에러(semantic error)는 존재하지 않는다. 다만 이상한 부
분을 찾아 보라! 답을 보지 말고, 꼭 찾아보기 바란다. 이것은 얼마나 흥미진
진한 발견인가?

#include <stdio.h>

void main() {

    int* i;

    i=new int;//i=(int*)malloc(sizeof(int));와 같다.

    *i=3;

    printf("%d\n",*i);

    delete i;//free(i);와 같다.

}

  결과는

3

이다.

  위의 프로그램을 Visual C++에서 실행하였다. 그러므로,

i=new int;

는 4바이트의 메모리를 할당하여 시작 주소를 i에 할당할 것이다. 그리고,

delete i;

는 i가 가리키는 4바이트를 해제할 것이다. 이상함을 느꼈는가? 아직 이상하
지 않다면, 그림을 그려보자. 그림을 그리는 것은 언제나 최선의 해결책이다.

             이상한 delete의 동작: (a) new에 의해 4바이트가 할당되고, 이곳을 i가 가
          리킨다고 하자. new가 할당한 곳은 [1000]이다. (b) delete는 i가 가리키는
          곳을 해제한다. i에는 오로지 [1000]밖에 없다. 어떻게 i가 가리키는 곳이
          [1000]~[1003]까지의 4바이트인지 알 수 있는가?

  new int; 에 명시한 int에 의해 new는 정확하게 4바이트를 할당할 것이다.
이곳이 [1000]번지에서 [1003]번지까지의 4바이트라 하자. 이제 i는 [1000]이라
는 값을 가질 것이다. main()의 마지막 문장은 i가 가리키는 [1000]번지를 해
제할 것이다. 더 정확하게 말하면, [1000]에서 [1003]까지의 4바이트를 해제할
것이다. 어떻게 이것이 가능한가? i에는 단지 [1000]만 있을 뿐, 4바이트가 할
당되었다는 정보도, 할당된 범위가 [1000]에서 [1003]까지란 정보도 없다!

  자 아래의 프로그램을 이제 Borland C++에서 실행해 보자. 새로운 함수
coreleft()를 호출하고 있는데, 이 함수는 힙(heap)에 남은 메모리의 크기를 바
이트로 리턴한다.

#include <stdio.h>

#include <alloc.h>

#include <conio.h>

void main() {

    char* cp;

    clrscr();

    printf("%ld\n",coreleft());//사용 가능한 메모리의 크기를 출력한다.

    cp=new char[12];

    printf("%ld\n",coreleft());

    delete[] cp;

    printf("%ld\n",coreleft());

}

결과는

474832

474816

474832

이다. 12바이트를 할당했는데, 왜 16바이트가 줄어든 것일까? 이것은 사용자가
지정한 크기 외에 4바이트가 추가로 할당된 것(볼런드 C++에서)을 의미한다.
이 4바이트에는 무엇이 들어있을까? 그렇다. 예상하다시피 할당한 메모리의
크기가 들어 있는 것이다!!!

  자 new는 이렇게 동작한다. new는

'사용자가 지정한 메모리의 크기+4바이트'

를 할당한다. 그리고,

'할당한 위치+4'

를 리턴한다. 그러므로, 위의 경우 할당해야 하는 메모리가 12바이트이므로,
new는 '12+4'바이트를 할당한다. new가 할당한 메모리의 위치가 [1000]에서
[1015]까지라고 하자. [1000]~[1003]에는 12를 넣는다. 그리고, new는 [1004]를
리턴한다. 그러므로, cp는 [1004]를 가리키게 된다. 하지만, 실제로는 1004번지
의 앞 4바이트로 할당되어 있다는 것을 기억하자.

  자 이제 delete는 이렇게 동작한다. delete[]⼆ cp; 에서 처럼, 할당한 메모리

             일단 []의 역할에 대한 설명은 뒤로 미루기로 하자.

의 시작 포인터를 지정하면, delete는

'지정된 포인터-4'

에 접근하여 할당된 메모리의 크기를 얻는다. 그리고, '지정된 포인터-4'에서
부터

'지정된 포인터-4'에 지정된 크기 + 4

바이트를 해제하는 것이다. 위의 예에서 delete[] cp; 는 cp가 가리키는 곳이
[1004]이므로, 먼저 [1004-4]번지에 접근한다. 그리고, 크기 12를 얻는다.

  그 다음에 [1000]에서부터, 16바이트의 메모리를 해제하는 것이다.

  new에서 메모리의 할당이 [1000]번지에서 부처 이루어졌음에도 불구하고,
왜 [1004]를 리턴하는 것일까? 그것은 실제로 데이터가 저장될 장소는 [1004]
이기 때문이다. 이러한 동작은 컴파일러가 생성한 코드에 의해서 숨겨지므로,
사용자는 단순하게 할당된 곳의 시작 포인터만으로도 메모리 할당 작업을 할
수 있는 것이다. 아래의 그림을 참고하자.

             new와 delete의 실제 동작: 볼런드 C++의 경우 동적 메모리 할당은 할당
          한 메모리의 크기를 유지하기 위해 여분의 4바이트를 더 할당한다. 이러한
          여분의 할당은 컴파일러마다 다르지만, 여분의 할당은 반드시 존재한다.
          사용자가 요구한 메모리의 크기가 12바이트라면 컴파일러는 16(12+4)바이
          트를 할당한다. 그리고, '할당한 위치+4'(1004)를 리턴한다. 후에 delete는
          [1004]앞의 4바이트에서 12를 읽어 메모리의 크기를 얻는다. 그리고 이 정
          보를 메모리 해제에 사용한다.

  하지만, 반드시 짚고 넘어가야 할 주의 사항이 있다. 독자들은 동적 메모리
할당이 항상 4바이트가 추가로 할당될 것이라고 가정(볼런드 C++에서조차)해
서는 안 된다. 위의 소스에서

cp=new char[12];

를

cp=new char[13];

으로 수정하면 결과는 어떻게 될까? 놀랍게도 결과는

474832

474800

474832

이다. 즉, 사용자는 1바이트를 더 할당했음에도 불구하고, 실제로는 32바이트
가 할당된 것이다(우리의 예상대로라면 17바이트가 더 할당되어야 한다).

최소 블락(minimum block): 패러그래프(paragraph)

  볼런드 C++의 도스용 버전의 이러한 동작은 세그멘테이션에 기인한다. 결
론을 말하면 다음과 같다.

"사용자가 동적 할당을 위해 지정한 바이트는, 항상 16의 배수로
할당한다."

  즉, 13을 지정하면, 17바이트를 할당해야하는데, 17을 포함하는 최소 16의
배수는 32이므로, 32바이트를 할당하는 것이다. 이것은 위에서 살펴보았듯이,
세그멘테이션 주소 지정 방식의 주소 표현 방식 때문이다. 메모리 블락의 베
이스 주소(base address)의 하위 4바이트는 사용할 수 없으므로, 항상 메모리
의 16바이트의 경계⼆에서 동적 할당이 이루어지는 것이다.

             이러한 16바이트의 경계를 '패러그래프 경계'라 한다. 하지만, 32비트 운
          영체제에서 더 이상 패러그래프 경계는 존재하지 않는다.

  이러한 사실을 모르는 초급, 혹은 중급 프로그래머가 가끔 실수하는 것은
바로 이러한 최소 블락의 크기를 모르는 원인에 기인한다. 자신이 할당하는
레코드의 크기가 12바이트라 하자. 그리고, 남는 메모리를 확인해 보니

20,000

바이트가 남아 있다. 중급 프로그래머는 더 할당할 수 있는 레코드의 갯수를
계산하기 위해

20,000/12=1,666

라는 식을 사용할 것이고, 상태라인(status line)에

'앞으로 1,666개의 레코드를 더 추가할 수 있습니다.'

란 메시지를 출력할 것이다. 하지만, 최소 블락의 크기가 16이라면

20,000/16=1,250

이며, 앞으로 1,250개의 레코드밖에 추가할 수 없는 것이다!

  각자가 사용하는 컴파일러의 최소 블락의 크기를 꼭 확인해 보라. 최소 블
락의 크기가 정해지면, 어떤 경우 한두 바이트를 추가로 할당하는 것은 오버
헤드가 아닐 수도 있다.

  단편화(fragmentation)

  위의 예에서 사용자가 메모리 블락의 크기로 13을 지정한 경우, 실제로 할
당한 메모리는 32바이트이지만, 이 중 17바이트만이 실제로 사용되고, 나머지,
15바이트는 사용하지도 못하고 놀게 된다. 이러한 현상을 메모리 내부 단편
화(memory internal fragmentation)라 한다. 최소 블락의 크기가 클수록 내
부 단편화로 못쓰는 메모리의 크기는 늘어 난다. 하지만, 후에 설명할 외부
단편화(external fragmentation)의 단점은 줄게 된다.

  디스크 블락(disk block)을 할당할 때에도 내부 단편화는 존재한다. Win+R
을 눌러 실행 대화상자(run dialog box)를 실행해 보자.

             Windows의 실행 대화상자: '열기'에 command를 입력하고 확인을 선택하
          면 도스창(DOS prompt)가 실행된다.

  도스창에서

CD \

를 입력하여 C: 드라이브의 루트(root)로 이동한 다음

DIR

을 입력하여 루트 폴더에 존재하는 파일을 리스트해 보자. 아래 그림은 필자
의 경우 실행 결과이다. 사용 가능한 디스크 공간이

1,434,173,440

인 것을 확인할 수 있다.

             DIR의 결과: 현재 사용 가능한 디스크 공간은 1,434,173,449바이트이다.

  자 이제 COPY 명령을 사용하여 5바이트 크기의 텍스트 파일을 만들어 보
자.

COPY CON TEST.TXT

라고 입력한다. 연속된 줄에

abc

^Z

를 입력한다. 이 때 ^Z는 Ctrl+Z를 입력한다(이것은 파일의 끝(EOF)을 나타내
는 특수 문자를 입력한다). 그러면 크기가 5⼆인 텍스트파일이 만들어진다.

             abc가 3바이트, 줄의 끝에 붙는 '\r\n'이 2바이트이다.

             크기가 5바이트인 test.txt 파일의 생성: 이렇게 파일을 만드는 방법은 DOS
          시절 많이 사용되었다. Windows 사용자는 notepad.exe를 사용하여 파일을
          만들어도 좋다.

  다시

DIR

을 입력하여 사용된 디스크 공간을 확인해 보자.

             클러스터(cluster)의 크기: 필자는 Win98의 FAT32(file allocation table for
          32bits)를 사용하기 때문에, 디스크 최소 블락, 즉 클러스터의 크기는 4096
          바이트이다. 그러므로, 새로 만들어진 파일의 크기는 5바이트임에도 불구
          하고, 4096바이트가 할당되었다. 4091바이트는 내부 단편화로 사용할 수
          없는 공간이다.

  위의 그림에서 보듯이, 5바이트 크기의 test.txt 파일이 만들어진 것을 확인
할 수 있다. 그리고 사용할 수 있는 디스크 공간은

1,434,169,344

로 4096바이트(212)가 줄어든 것을 알 수 있다. 그러므로, 디스크 할당 시 최소
블락의 크기는 4096바이트이다. 일반적으로 운영체제의 파일 시스템(file
system)에서는 이러한 최소 블락을 클러스터(cluster)⼆라고 한다.

             실제로 디스크를 포맷(format)하면, 섹터(sector)라는 최소 단위로 나뉘어
          진다. 운영 체제는 이러한 섹터를 여러 개 포함하는, 클러스터라는 최소단
          위로 관리한다. 만약 섹터의 크기가 1024라면, FAT32에서는 4개의 섹터를
          1개의 클러스터로 관리한다. 독자들의 파일 시스템은 FAT32가 아닐 수도
          있다. 클러스터의 크기가 크다면 디스크 내부 단편화(disk internal
          fragmentation)로 못 쓰는 공간이 늘어나게 된다.

  단편화에는 또한 외부 단편화가 존재한다. 독자들은 디스크 외부 단편화
(disk external fragmentation)를 제거하는 프로그램을 '시작->프로그램->보
조 프로그램->시스템 도구->디스크 조각 모음'에서 찾을 수 있다.

             디스크 외부 단편화 제거 프로그램: 클러스터의 위치를 옮겨, 빠르게 디스
          크 접근(access)을 할 수 있도록 재배치한다.

  디스크 외부 단편화의 자세한 방법은 설명하지 않는다. 하지만, 독자들이 아
래에서 설명하는 메모리 외부 단편화(memory external fragmentation)를
이해한다면, 디스크 외부 단편화의 원리를 쉽게 이해할 수 있다.

외부 단편화(external fragmentation)

delete i;

와

delete[] i;

는 어떻게 다를까? 이것을 이해하기 위해서는 메모리 외부 단편화(external
fragmentation)를 이해하는 것이 필요하다. 아래 그림과 같은 메모리 할당을
생각해 보자. i(1024)의 형태는 i 블락이 1024바이트 할당되었음을 나타낸다.

             메모리 블락의 할당: (a) 6,656바이트의 메모리가 모두 할당된 상태. 사용
          가능한 메모리 공간은 0바이트이므로 더 이상 동적 메모리 할당이 불가능
          하다. (b) 사용자가 j,k와 m블락을 해제했다. 이제 사용 가능한 메모리 공간
          은 2,560바이트이다.

  메모리가 위의 그림 (a)처럼 모두 할당되어서 사용 가능한 메모리 공간은 0
바이트이다. 사용자가 j,k와 m블락을 해제했다. 이 그림은 (b)인데 이제 사용
가능한 공간은 2,560바이트이다.

             2,048바이트의 할당: (a) 사용자가 2048바이트의 새로운 메모리 블락의 할
          당을 요구했다. 하지만, 연속된 메모리 공간을 할당해야 한다면 이것은 불
          가능하다. (b) i,j와 n블락의 위치를 옮기면 이제 2048바이트의 연속된 메모
          리 공간을 할당하는 것이 가능하다. 이렇게 연속된 메모리 블락의 할당을
          위해 메모리 블락을 옮기는 것은 운영체제의 지원이 필요하다. 이러한 작
          업을 컴팩션(compaction)이라 한다. DOS는 컴팩션을 지원하지 않는다. 이
          것은 조심스러운 코딩을 요구한다.

  사용자가 다시 2048바이트의 메모리 블락을 요구했다고 하자. 사용 가능한
메모리는 비록 2,560바이트임에도 불구하고, 2048바이트를 할당하는 것은 불가
능하다. 왜냐하면, 대부분의 컴파일러는 메모리 블락을 할당할 때, 연속된 메
모리 블락을 요구하기 때문이다. 이러한 상황을 외부 단편화(external
fragmentation)라 한다. 즉 메모리 외부 단편화란

"할당된 메모리 블락이 연속적이지 않고, 불연속적인 상태"

를 말한다. 이러한 외부 단편화는 운영 체제의 감독하에 제거되는 것이 바람
직하다. 이러한 외부 단편화 제거를 '압축(compaction)'이라 한다. 하지만,
DOS는 컴팩션을 지원하지 않는다. 그러므로, 도스에서 동적 메모리 할당은
조심스러운 코딩⼆을 요구한다.

             독자들 중 대부분은 이러한 외부 단편화 때문에 고생한 적이 없을 것이
          다. 필자는 DOS용 한글 라이브러리를 제작하면서 이러한 외부 단편화 때
          문에 심하게 고생한 적이 있다. 필자는 결국 디스크에 메모리 블락을 쓰는
          (write) 방법으로 이러한 외부 단편화 문제를 해결했다. 하지만, Win32에서
          는 본질적으로(inherently) 외부 단편화가 존재하지 않는다. 이러한 사항은
          이어지는 절(section)에서 살펴보도록 하자.

  하지만, Windows에서는 페이징(paging)을 사용하므로, 근본적으로 외부
단편화는 발생하지 않는다.

  이제 두 가지 delete의 용법을 설명한 준비가 모두 끝났다.

new int;

와

new int[10000];

은 본질적으로 다르다. 물론 이것은 정수 1개를 할당하는 것과, 정수 10000개
를 할당하는 차이 외에 더 중요한 차이가 존재한다.

  브래킷([)을 사용하지 않는 new는 반드시 연속된 메모리 블락을 할당한다.
즉, 외부 단편화가 존재하면 메모리 할당을 실패할 수도 있다. 하지만, 브래킷
을 사용하는 new는 연속적이지 않은 메모리 블락에 대해서도 할당을 시도한
다. 즉, 위의 그림 (a)에서처럼 외부 단편화가 존재하더라도, 브래킷을 사용하
는 new는 (연속적이지 않더라도 전체 메모리가 충분하다면) 성공할 가능성이
있다.

  그러므로, delete도 두 가지 new에 대해서 다르게 동작해야 한다.

delete i;

는 반드시 연속된 메모리 블락을 해제한다. 하지만,

delete[] i;

는 i에서 시작하는 연속적이지 않은 메모리 블락에 대해서 해제를 시도한다.

  만약,

i=new int[10000];

이 재수 좋게도 연속된 메모리 블락에 할당되었다면,

delete i;

와

delete[] i;

는 같다. 하지만, 배열 형식의 new에 대해서는 반드시 delete[]를 사용하는 것
이 바람직하다.

   가상 메모리(virtual memory)

  가상 메모리(vitual memory) 및 스왑 파일(swap file)은 'Win32 메모리 모
델'에서 자세히 설명한다. 독자들은 'Win32 메모리 모델'에서 핸들(handle)
을 사용하는 메모리 할당 함수(memory allocation function)의 이유에 대해서
도 좋은 정보를 얻을 수 있을 것이다.

   직접 메모리 접근(direct memory access)

  DOS 시절 한 때, 할당하지 않은 시스템 영역의 메모리를 직접 접근하는 것
이 가능했다. 하지만, Windows에서는 이것이 불가능하다. 사용자는 오로지,
자신이 할당한 메모리 영역에 대해서만, 직접적인 접근을 하는 것이 가능하다.

  이제 이러한 기교가 쓸모 없는 것은 사실이지만, 비디오 램(video RAM)의
구조를 이해하기 위해, 또한 2차원의 인덱싱(indexing)을 위한 몇 가지 기교를
살펴보기 위해 DOS에서 비디오 램을 직접 접근하는 것을 구현해 보자.

텍스트 모드 비디오 램의 구조

  VGA(video graphic array) 표준 80×25, 16컬러, 텍스트 모드(text mode)에
서 화면에 나타나는 정보는 메모리의 0xb8000에 위치한다. 이곳을 비디오 램
(video RAM)이라 한다. 그러므로 비디오 램의 주소는 0xb800:0이다.

  텍스트 화면 전부를 위해 몇 바이트의 비디오 램이 필요할까? 80×
25=2,000byte라고 생각해서는 안 된다. 글자를 나타내는데 1바이트(ASCII 코
드), 글자의 색,배경색,깜빡거림(속성)을 나타내기 위해 1바이트를 사용하므로,
모두 4,000byte가 필요하다. 즉, 화면에 나타나는 글자 1개는 실제로 메모리의
2바이트로 저장된다. 아래 그림을 보자.

             VGA 텍스트 모드 비디오 램의 구조: 화면의 좌측 상단은 0xb800:0∼
          0xb800:1의 2바이트에 저장된다. 두 번째 바이트는 글자의 속성(attribute)을
          나타낸다.

  글자에 대응하는 처음 주소에는 글자의 아스키코드가 1바이트로 저장되며,
2번째 주소에는 글자의 속성이 1바이트로 저장된다. 속성 바이트의 의미는 아
래 그림과 같다.

             속성(attribute)의 의미: 하위 4비트는 글자색을 나타낸다. 그 위의 3비트는
          글자의 배경색이다. 최상위 비트는 깜빡거림 유무를 나타낸다.

  글자색은 4비트를 사용하므로, 16색을 나타낼 수 있으며, 배경색은 3비트를
사용하므로 8색을 나타낼 수 있다. MSB가 1이면 글자는 깜빡거린다. 각 비트
위치의 색에 대한 매크로 상수가 conio.h에 정의되어 있으므로(반드시 볼런드
계열의 컴파일러를 사용해야 한다), 참고하기 바란다. 예를 들면 BLACK=0,
BLUE=1 등이다.

  화면의 좌측 상단(left top)을 원점 (0,0)이라고 했을 때, 화면의 해상도
(resolution)는 80×25이며, 우측 하단의 좌표는 (79,24)이다. 화면의 임의의 위
치 (x,y)에 글자 ch를 속성 attr로 찍는 함수 PutCh(x,y,ch,attr)을 만들어 보
자.

  아래 그림을 참고하여, (x,y)에 해당하는 비디오 램의 위치를 계산해 보자.

             비디오 램의 위치 계산: 가로줄의 해상도는 80이지만, 모두 160바이트를
          차지하므로, y*160으로 계산해야 한다.

  먼저 y좌표를 이용하여, 세로의 시작 위치를 구한 다음 x*2로 가로의 위치
를 구하면 된다. 그러므로, 비디오 램의 시작 주소를 video라고 하면, (x,y)위
치는

video+y*160+x*2

이다. 비디오 램의 시작 위치를 포인터 변수로 변환하기 위해 dos.h에 선언된

MK_FP(base,offset)

를 사용할 수 있다. 이것은 MaKe Far Pointer의 의미로, base:offset 주소를
32비트로 생성⼆한다.

             DOS에서 주소는 20비트인데 왜 32비트로 포인터를 유지하는가? 32비트
          의 상위 16비트에는 베이스 주소를 유지하고, 하위 16비트에는 오프셋 주
          소를 유지한다. MK_FP의 소스를 살펴보라. 이 매크로 함수는 단순히 base
          를 16비트 왼쪽 쉬프트(left shift)한다.

  아래에 소스를 리스트하였다.

#include <stdio.h>

#include <dos.h>

char far* video=(char far*)MK_FP(0xb800,0x0000);

void PutCh(int x,int y,char ch=' ',char attr=0x07) {

    int offset=y*160+x*2;

    *(video+offset)=ch;

    *(video+offset+1)=attr;

}

void main() {

    PutCh(0,0,'A',0x97);

    PutCh(79,24,'B');

}

  결과는 아래 그림과 같다.

             A는 (0,0)의 위치에 파란(1) 배경에 회색 글자가 깜빡거리며, B는 (79,24)
          위치에 있으며, 커서의 위치는 변하지 않았다(커서는 0,24 위치에 있다).

  속성이 0x97이면,

1001 0111

이므로, 글자색 회색(7), 배경색 파랑(1), 또한 글자는 깜박거린다.

  아래의 예는 토글 키(toggle key) 및 쉬프트 키들(shift key set)의 상태를
저장하는 메모리에 직접 접근하는 소스이다.

#include <stdio.h>

#include <conio.h>

#include <dos.h>

void ShiftState()

{

    #define CAPSLOCK 0x40

    #define NUMLOCK  0x20

    #define ALTKEY   0x08

    #define CTRLKEY  0x04

    #define LSHIFT   0x02

    #define RSHIFT   0x01

    unsigned char shift = peekb(0,0x417);

    while(!kbhit()) {

        shift = peekb(0,0x417);

        if (shift & LSHIFT) printf("lshift ");

    }

}

int main(void)

{

    clrscr();

    return 0;

}

  이 프로그램을 실행하여 왼쪽 shift를 누르면,

lshift

가 계속 찍힌다. 다른 문자 키를 누르면 프로그램은 종료한다.

  Shift, Alt, Ctrl 및 Win키는 눌렀을 때 키 값이 발생하는 키가 아니라, 누르
고 있는 동안 상태가 지정되는 키이다. 이러한 키 들을 쉬프트 키 집합(shift
key set)이라 한다. DOS에서 이 값은 0000:0417에 저장된다. Windows에서는
위의 함수 대신 API 함수 GetKeyState()를 사용할 수 있다.

   연습문제

1. 최소 블락의 크기가 32이고, 여분의 메모리로 4바이트가 할당 더 되는 어떤
시스템에서, 사용자가 유지하는 레코드 블락의 크기를 30이라고 하자. 현재 사
용 가능한 메모리가 20000이라면, 몇 개의 레코드를 더 할당할 수 있는가?

2. 클러스터의 크기가 4096일 때, 사용자가 10,000바이트의 파일을 만들면 디
스크 내부 단편화로 사용할 수 없는 공간은 몇 바이트인가?

3. 컴팩션(compaction)은 어느 시점에 행해지는가? 또한 컴패션 알고리즘에는
어떤 종류가 있는지 설명하라.

4. Win32의 파일 시스템 같은, 페이징(paging) 시스템에서는 왜 컴팩션이 필
요 없는가?

5. 비디오 램의 시작 위치를 구하는 아래의 문장에서

char far* video=(char far*)MK_FP(0xb800,0x0000);

왜 far 포인터를 사용해야 하는가?

6.


23. 인터럽트(interrupt)

  DOS가 컴퓨터를 주름잡던 시절, 인터럽트(interrupt)가 있었다. DOS가
Windows에게 주도권을 빼았긴 뒤, 인터럽트는 사람들에게서 잊혀져 갔다. 하
지만, 그는 서브클래싱(subclassing)이라는 다른 모습으로 언제나 우리들 곁
에 남아 있다.

   키보드는 어떻게 동작하는가?

  CPU가 어떻게 키보드의 입력을 받아들이는 것일까? CPU는 다른 일로 너
무 바빠서 키보드의 입력을 항상 신경 쓸 수는 없다.

  결론을 말하면, 하드웨어 주변 장치는 CPU에게 인터럽트(interrupt)라는 방
식으로 자신의 입력을 전달한다.

  우리는 프로그램에서 키보드 입력을 받아들이기 위해 어떤 방법을 사용하는
가? 예를 들어, 0에서 100사이의 난수를 발생하면서, 사용자가 ESC를 입력하
면 종료하는 프로그램에 대해서 생각해 보자. 아마도 코드는 다음과 같을 것
이다.

...

while (1) {

    printf("%d\n",random(100));

    if (kbhit()) {

        ch=getch();

        if (ch==ESC) break;

    }

}

...

  놀랍게도 위의 소스를 완벽하게 이해하고 있는 독자는 거의 없을 것이다.
자 차근히 소스에서 발견되는 문제점을 살펴보자.

① 어디서도 (실제로) 키보드에서 입력을 받는 루틴은 존재하지 않는다. 키보
드 입력은 누가 받는가?

② kbhit()은 도대체 무엇을 검사하는가?

③ 분명히 프로그램은 실행되고 있다. 또한, 키보드 입력도 가능하다. 어떻게
2가지 일의 동시 처리가 가능한가?

키보드가 알린다

  놀랍게도 키보드 입력은 CPU에 의해서가 아니라, 키보드에 의해서 시작된
다. CPU는 키보드 입력은 전혀 신경 쓰지 않고 있다가, 키보드의 신호에 의
해 키 입력을 받는다. 즉, CPU는 자신의 일을 하고 있다가 키보드가

'지금 사용자가 키를 눌렀으니, 키 값을 받으시오'

라고 신호를 보내면, CPU는 자신이 하던 일을 잠시 중단하고, 키보드로부터
키 입력을 받는 것⼆이다. 즉, CPU 입장에서 키보드는 끼어들기(interrupt)를

             이것을 인터럽트 서비스 루틴(ISR: Interrupt Service Routine)이라 한
          다. 즉, 인터럽트가 요청되었을 때, 각 인터럽트마다 실행할 함수를 메모리
          에 가지고 있는데, 이 함수를 ISR이라 하는 것이다.

한 것이다. 그래서, 키보드가 보내는 이러한 신호를 인터럽트 요청(interrupt
request: IRQ)이라 한다. DOS의 경우, (소프트웨어) 인터럽트의 종류는 모두
256개가 있을 수 있지만, 실제로 사용하는 인터럽트는 이 중 약 50%정도 만
이다. 예를 들면, 키보드 인터럽트는 9번이다.

  그렇다면, 인터럽트 요청을 어떻게 확인할까? CPU가 인터럽트 요청을 확인
하기 위해서, 시간을 소모해야 한다면, 인터럽트는 무의미한 것이다. 하지만,
CPU가 인터럽트가 요청되었는지 확인하는데는 전혀 시간을 사용하지 않는다.
즉, 인터럽트가 걸리면 인터럽트 번호와 함께 CPU가 인터럽트를 인식하게 되
는 것이며, 인터럽트가 걸리지 않는다면, CPU는 자신이 하던 일(그것이 수치
계산이든, 파일 저장이든)을 계속한다. 이것이 실제로 수행되는 것은 신비롭지
만, 조합 논리 회로(combinational logic circuit)에 의해서, 이것은 가능하
다. 독자들이 만약 대학에서 컴퓨터공학을 배운다면, 2,3학년 쯤에 '논리 회로
(logic circuit)' 과목에서 이러한 회로를 배우게 될 것이다.

  CPU가 키 입력을 받아들이는 다른 방법으로 우리가 흔히 하는 방법을 생
각해 볼 수 있다. 즉, 일정 시간마다 하던 일을 멈추고 키보드를 검사하여, 키
입력이 있으면, 값을 입력받는 것이다. 이것은 CPU에게 너무 부담이 크며
(CPU가 할 일은 키보드 검사 외에도 너무 많다), 동기화(synchronization)(키
를 눌렀을 때 즉시 입력받지 못한다)도 되지 않는다. 이러한 방식을 바쁜 기
다림(busy waiting)이라 한다. 실제로, 위에서 예로 든 프로그램은 사용자가
입력한 키 값이 ESC인지 검사하기 위해, 바쁜 기다림을 하고 있다(Windows
에서 키 입력은 이러한 방식으로 하지 않는다).

  키보드 인터럽트(keyboard interrupt)가 걸리면(사용자가 키를 입력하면),
CPU는 키보드에서 키를 입력받는다. 사용자가 타이핑 속도가 빨라서, CPU가
너무 많은 키보드 인터럽트를 받을 것에 대해서는 걱정하지 않아도 좋다.
CPU의 속도에 비하면, 사용자가 10,000타를 친다하더라도 인터럽트의 간격은
너무 느린 것이다!

  입력된 키 값은 운영체제가 보관한다. 운영체제가 키 값을 보관하는 메모리
공간을 키 버퍼(key buffer)라 한다.

  예를 들어, 사용자가 dir이라고 입력한 경우, 키 버퍼의 모양은 아래 그림과
같다.

             키보드 인터럽트의 처리: 키보드 인터럽트가 발생하면, CPU는 하던 일을
          멈추고 키보드에서 키 값을 입력받아 메인 메모리(main memory)의 키 버
          퍼(key buffer)에 키 값을 저장한다. 키 값은 스캔 코드(scan code)가 저
          장되지만, 일단은 ASCII 코드 값이 저장된다고 알아두자. 위의 그림은 사용
          자가 d,i,r을 입력한 경우의 키 버퍼의 상태이다. kbhit() 함수는 키 버퍼에
          있는 키의 수(지금은 3)를 리턴한다.

  키 버퍼에 입력된 문자는 프로그램에서 명시적으로 가져가지 않으면, 계속
해서 남아 있다. 키 버퍼에서 1문자를 아스키 값으로 읽어오는 함수가

getch()

이다. getch()는 키 버퍼가 비어 있다면, 키 버퍼가 찰 때까지 기다린다는 것
을 주의하라. 그러므로, 사용자가 어떠한 키도 입력하지 않은 경우, getch()는
키 입력을 무한정 기다리게 된다. 또한, 키 버퍼에 있는 문자의 수를 리턴하는
함수는

kbhit()

이다. kbhit()⼆은 is KeyBoard Hit?의 의미인데, 이것은 단순히 키 버퍼에 있

             이것은 키보드와 관계된 함수이지만, 표준 함수가 아니다. 이 함수의 선
          언은 conio.h에 들어 있다.

는 문자의 수를 리턴한다. 만약 키 버퍼가 비어있다면, 0을 리턴할 것이다.

  위의 경우, kbhit()호출은 3을 리턴한다. 그리고, 첫 번째 getch() 호출은
'd'(=100)를 리턴한다.

             getch()의 동작: getch()는 키 버퍼에서 첫 번째 문자를 꺼내온다. 이제 키
          버퍼에는 i,r이 남아 있을 것이다.

  다시 한번 강조한다.

"키 입력을 받는 함수는 getch()가 아니다. getch()는 단지 입력된 키
값을 읽어올 뿐이다. 키 입력은 키보드 인터럽트를 처리하는 운영체제가
담당한다."

   자세한 내부

  그래도 여전히 의문이 남는다. CPU가 인터럽트를 식별했을 때, 잠시 하던
일을 멈추고 인터럽트 서비스(interrupt service) - 키보드 인터럽트의 경우,
키 값을 입력받는 일 - 를 한다고 했는데, 이것은 어떻게 가능하면, 어디에
있는 루틴을 실행하는 것일까?

  예를 들면, 9번 인터럽트가 요청되었을 때, 9번 인터럽트를 위한 인터럽트
서비스 루틴(ISR)의 시작 주소를 어떻게 결정할까?

             인터럽트 벡터 테이블과 ISR: 메모리의 하위 256×4바이트는 ISR의 시작
          주소를 가리키는 테이블이 존재한다. 이곳을 인터럽트 벡터 테이블(IVT:
          interrupt vector table)이라 한다. 9번 인터럽트가 걸리면, 9×4=36(0x24)번
          지에 접근하여, 먼저 9번 ISR의 시작 주소를 얻는다. 그리고, 이 함수를 호
          출한다.

인터럽트 벡터 테이블(IVT)

  메모리의 하위 256×4=1,024바이트에는 각 인터럽트 서비스 루틴의 시작주
소가 4바이트씩 들어 있다. 이것을 인터럽트 벡터 테이블(Interrupt Vector
Table)이라 한다. 예를 들면, 9번 인터럽트의 ISR의 시작 주소는 4×9=36번
지에 주소가 들어 있다. 만약 CPU에 9번 인터럽트가 걸리면, CPU는

9×4=36

을 계산하여, 36번지에 접근하여 먼저 9번 인터럽트 서비스 루틴의 시작 주소
를 얻는다. 그 값이

F000:5000(16진수)

이라면, CPU는 그 함수를 호출하는 것이다. 물론, 함수를 호출하기 전에 CPU
의 상태를 스택에 저장한다. 인터럽트 서비스 함수가 리턴하면 CPU는 다시
원래의 일을 한다.

  ISR도 메모리에 존재하는 하나의 함수라는 사실을 주목하라. 그러므로 파라
미터를 받을 것이고, 값을 리턴할 것이다. 일반 함수는 파라미터를 전달하기
위해서 메모리의 스택을 사용하지만, ISR은 속도를 위해서, 메모리를 사용하
지 않는다. 그렇다면, ISR에 어떻게 파라미터를 전달할까?

             8086의 레지스터: 일반 목적 레지스터(general purpose register)로 AX, BX,
          CX와 DX(Accumulator, Base, Counter, Data를 의미)를 가지고 있다. 세그먼
          트 베이스 레지스터로 CS, DS, SS 및 ES(Code, Data, Stack, Extra
          Segment를 의미)를 가지고 있다. 또한, 연산의 결과를 저장하는 Flags 레지
          스터를 가진다. AX 레지스터 등은 16비트인데, 상위 8비트는 AH(High), 하
          위 8비트는 AL(Low)로 나뉘어서 사용할 수 있다. PentiumIII 프로세서 등은
          하위 호환을 위해 16비트 레지스터를 가지면, 32비트, 64비트 레지스터도
          가진다.

  그렇다. CPU 내부의 메모리인 레지스터(register)를 사용하여 파라미터를
전달할 수 있다. 자, 인터럽트 서비스 루틴의 다음 사실을 기억하자.

"인터럽트 서비스 루틴은 파라미터를 전달받기 위해 지정된 레지스터를
사용하며, 리턴값도 지정된 레지스터에 저장한다."

  즉, 모든 인터럽트 서비스 루틴은 파라미터를 어느 레지스터에 받을지 (하
드웨어 설계자에 의해) 지정되어 있다. 그리고, 그 루틴이 값을 리턴한다면,
리턴값을 어느 레지스터에 저장할지도 지정되어 있는 것이다⼆.

             각 인터럽트에 대한 상세한 설명은 Norton Guide란 오래된 하지만, 유명
          한 프로그램을 통해 알 수 있다. 혹은, 옛날 DOS 서적이나 C 서적에 설명
          이 있을 것이다.

  자 위의 예를 다시 리스트하였다.

#include <stdio.h>

#include <conio.h>

#include <stdlib.h>

#define ESC 27

void main() {

    char ch;

    while (1) {

        printf("%d\n",random(100));

        if (kbhit()) {

            ch=getch();

            if (ch==ESC) break;

        }//if

    }//while

}

  프로그램의 설명은 다음과 같다.

  프로그램이 실행하면 while 루프의 kbhit()은 바쁜 기다림(busy waiting)으로
키 버퍼에 있는 문자의 수를 가져온다. 키를 입력하지 않았다면, kbhit()은 계
속해서 0을 리턴할 것이다. 사용자가 ESC를 입력하면, 키보드는 CPU에 인터
럽트 9번을 요청한다. CPU는 자신이 하던 일을 멈추고 ISR 9를 실행하여, 입
력한 키 값을 키 버퍼에 저장한다. 다시 kbhit()이 실행할 때, 이번에는 1을 리
턴하므로, if문의 조건은 만족되어 if문으로 진입한다. getch()는 키 버퍼에서 첫
번째 키 값을 읽어온다. 그 값이 27이므로, break문이 실행되면, while문을 탈
출한다. 프로그램은 종료한다.

   ISR을 직접 호출

  프로그램에서 직접 ISR을 호출할 수는 없을까? 물론 할 수 있다. 프로그램
에서 ISR을 호출했을 때, 이것은 엄격한 의미에서 인터럽트가 아니다. 이러한
호출을 실제의 하드웨어 인터럽트(hardware interrupt)와 구분하기 위해 소
프트웨어 인터럽트(software interrupt)⼆라 한다. 그리고, 프로그래머들이

             하드웨어 인터럽트, 소프트웨어 인터럽트 및 트랩(trap)은 비슷하지만, 구
          분되어야 한다. 트랩은 소프트웨어 인터럽트의 일종이지만, 분명히 다르다.
          소프트웨어 인터럽트는 프로그램 소스에 명시되므로, 언제 호출될지를 분
          명히 예측할 수 있다. 하지만, 트랩은 예측할 수 없는 소프트웨어 인터럽
          트를 말한다. 예를 들면, 디버거에서 추적(trace)은 언제 발생할지 예측할
          수 없다.

일반적으로 인터럽트를 사용한다라고 하면, 소프트웨어 인터럽트를 의미하는
것이다.

  특정한 인터럽트를 호출하기 위해서는, 당연히 그 ①인터럽트 번호와 ②파
라미터를 넣어줄 레지스터 및, ③리턴 값이 저장되는 레지스터를 알고 있어야
한다(독자들은 레지스터에 대한 정확한 사용을 모르고 있을 것이므로, 아래의
예들은 난해할 것이다. 하지만, 이 장에서 인터럽트 서비스 루틴을 가로채는
기교와 개념에 대해서는 반드시 익혀 두라. 그것은 후에 Windows 프로그래
밍에서 Subclassing이라는 다른 단어로 나타날 것이다).

  아래의 예를 보자. Borland C++ 3.1에서 CPU의 레지스터를 직접 접근하기
위해, 전역 변수

_AX

등을 사용할 수 있다. 또한, 어셈블리 명령을 C++ 소스에서 직접사용하기 위
해,

_asm 혹은 asm

키워드를 사용할 수 있다. 이렇게 사용하는 어셈블러를 인라인 어셈블러
(inline assembler)라 한다.

  아래의 예는 clrscr()에 대응하는 ClearScreen() 및 gotoxy()에 대응하는
GotoXY()를 인터럽트를 사용하여 직접 만든 것이다. 레지스터에 할당하는 값
을 상세히 알려 하지 말라. 그것은 인터럽트를 설명한 문서에 자세히 설명되
어 있다.

#include <stdio.h>

void ClearScreen()

{

    _AX=0x0600;//인터럽트 함수의 파라미터를 설정한다.

    _BH=0x0b;

    _CH=0x00;

    _CL=0x00;

    _DH=0x32;

    _DL=0x4f;

    _asm int 0x10;//10번 인터럽트를 호출한다.

                  //i8086의 인터럽트 호출 어셈블리 명령은 int <번호>이다

}//ClearScreen

void GotoXY(int x,int y)

{

    _AH=0x02;

    _BH=0x00;

    _DH=y;

    _DL=x;

    _asm int 0x10;

}//GotoXY

void main()

{

    printf("Hello");

    GotoXY(5,5);

    printf("World");

}//main

  Borland C++는 CPU 레지스터에 대응하는 레지스터 공용체(union)⼆를 미리

             AX를 AH,AL로도 사용하므로, 공용체가 적당한 자료 구조(data structure)
          이다. 이 부분의 설명은 '구조체'를 참고하라.

만들어 두었다. 그것은 dos.h에 선언되어 있으며, 형 이름은 union REGS⼆ 이

             C에서 union은 형 이름에 반드시 포함되어야 한다. 이것은 union REGS
          가 typedef가 아닌 union으로 형이 정의되었기 때문이다. C++에서 union은
          생략해도 좋다. 이것은 'C++의 구조체'를 참고하라.

다. union REGS를 이용한 GotoXY()의 소스는 아래와 같다.

void GotoXY(int x, int y)

{

    union REGS regs;

    regs.h.ah = 2;  /* set cursor position */

    regs.h.dh = y;

    regs.h.dl = x;

    regs.h.bh = 0;  /* video page 0 */

    int86(0x10, &regs, &regs);

}

  int86()함수는 i8086 인터럽트를 호출하는 함수이다. 두 번째 파라미터는 입
력 레지스터, 세 번재 파라미터는 인터럽트의 결과가 넘어오는 레지스터 공용
체의 시작 주소를 명시한다.

  아래의 예는 인라인 어셈블러 및 인터럽트를 사용하여, 320×200, 256 컬러
그래픽 모드에서 점을 찍고, 팔레트를 설정하는 예이다. 이 예는 지금 소화하
기에는 벅차다. 설명은 필자의 다른 책 "Problem Kernel"에서 하도록 하겠
다.

#include <stdio.h>

#include <conio.h>

#include <dos.h>

#define txt2grp() _asm { mov ax,0x13; int 0x10 }

#define grp2txt() _asm { mov ax,0x03; int 0x10 }

#define putpxl(x,y,c) (*(unsigned char far *)(0xa0000000L+(x)+(long)\

                      (y)*320)=(c))

#define getpxl(x,y) ((unsigned char far *)(0xa0000000L+(x)+(long)\

                    (y)*320))

void set_palette_using_port(unsigned char *data)//768 entry data

{

    int i;

    unsigned char R,G,B;

    outportb(0x3c8,0);//0 is starting color index of converting

    for (i=0;i<768;i+=3) {

        R=data[i]; G=data[i+1]; B=data[i+2];

        outportb(0x3c9,R); outportb(0x3c9,G); outportb(0x3c9,B);

    }//for

}//set_palette_using_port

void set_palette_using_interrupt(unsigned char *data)

{

    _AH=0x10;

    _AL=0x12;

    _BX=0x00;//starting DAC color index for converting

    _CX=0xff;//number of DAC color entry for converting

    _ES=FP_SEG(data);

    _DX=FP_OFF(data);

    _asm int 0x10;

}//set_palette_using_interrupt

void main()

{

    int x,y;

    txt2grp();

    for (x=0;x<320;++x)

        for (y=0;y<200;++y)

            putpxl(x,y,(x+y)%256);

    getch();

    grp2txt();

}//main

  실행 결과는 아래 그림과 같다.

             320×200, 256컬러모드에서 현재의 팔레트 색으로 화면 전체에 점을 찍는
          다.

   TSR? Subclassing!

  자, 이제 인터럽트를 사용한 흥미 있는 일을 해 보자. 바로 인터럽트 서비스
루틴을 우리가 가로채는 것이다! 또한, 우리는 램상주(Terminate & Stay
Resident: TSR)라는 특별한 인터럽트를 사용할 것이다. 이것은 종료하지만,
메모리에 남아 있는 응용 프로그램을 의미한다. Windows에서 여러 프로그램
을 실행하는 멀티태스킹(multitasking)⼆은 일반적인 것이지만, DOS에서 멀

             디스크의 프로그램이 메모리에 로드(load)되어 실행 중일때, 그 프로그램
          을 태스크(task) 혹은 프로세스(process)라 한다. 멀티태스킹은, 여러 사람
          이 1대의 컴퓨터를 사용하는 것을 가리키는 멀티프로그래밍(multi
          programming)과는 구분되어야 한다.

티태스킹은 램상주 기법으로 제한적으로만 가능했었다. 놀랍게도 DOS에서도
멀티태스킹은 가능했었다!

ISR 가로채기

  9번 인터럽트(키보드 인터럽트)가 걸리면, 9×4=36번지에서 9번 ISR의 시작
주소를 얻는다. 그리고, 이 주소의 루틴을 호출한다. 그렇다면, 우리가 36번지
에 있는 주소를 바꾼다면, 키보드 인터럽트가 발생할 때 운영체제는 우리가
설치한 주소가 가리키는 함수를 호출할 것이다. 이것은 운영체제 입장에서 완
전한 불법(invalid)이다. 하지만, 보호(protection)을 전혀 수행하지 않던 DOS
에서 이것은 가능했다!

             인터럽트 가로채기: 36번지에 있는 원래의 ISR의 주소인 F000:5000을 보
          관한다. 그리고 새로 만든 함수의 시작 주소 0x600000을 대입한다. 새로
          설치한 함수의 처음에서 F000:5000에 있는 ISR을 호출하는 코드를 작성한
          다. 9번 인터럽트가 발생하면, ①0x600000에 있는 함수가 불린다. 그리고
          ②원래의 ISR9()를 호출한다. ③ISR9()가 리턴하면, 추가적인 일을 한 다음,
          ④다시 CPU에게 제어를 넘긴다.

  36번지의 값을 읽어서 이것을 보관한 다음, 거기에 우리가 만든 함수의 주
소(예를 들면 0x600000)를 집어넣는다. 키보드 인터럽트가 발생하면, 우리가
설치한 함수인 0x600000에 있는 함수가 불릴 것이다. 이 함수에서는 원래의
키보드 처리를 하기 위해 원래 ISR을 불러 준 다음, 우리가 해야 할 일을 추
가한다. 그러면, 키보드 인터럽트에 특별한 기능을 추가하는 것이 가능하다.

  인터럽트 벡터 테이블에서 주소값을 얻어오기 위해

getvect(interrupt_number)

를 사용한다. getvect()는 파라미터로 인터럽트 번호를 받아, 4바이트 주소를
리턴한다. 또한, 특정한 위치에 주소값을 써 넣기 위해,

setvect(interrupt_number,address)

를 사용한다. 이것은 interrupt_number 인터럽트의 ISR을 address로 설정한
다. 인터럽트 지원은 DOS에서만 가능하다. 그래서 이러한 함수의 원형
(prototype)은 dos.h에 있다.

  8번 인터럽트인 타이머 인터럽트(timer interrupt)는 1초에 18번 불린다
(이것은 Windows에서 TIMER를 18개 이상 만들지 못하는 이유이다). 타이머
인터럽트는 현재 시간을 갱신한다(메모리의 특정한 곳에 현재 날짜와 시간을
8바이트로 써 넣는다).

  타이머 인터럽트를 가로채서, 얼굴이 움직이게 해 보자. 아래의 소스에서 보
면 어디에도 반복문(loop)은 없다. 하지만, 까만 얼굴은 계속해서 화면을 돌아
다닌다(타이머 인터럽트는 계속해서 발생한다). 더군다나, 다른 프로그램을 실
행하는 것도 가능하다. ISR에서 다른 ISR이 불리는 것을 막기 위해, 문자를
찍는 함수는 앞 장에서 다루었던, DMA 루틴을 사용하고 있다.

  하지만, 독자들이 아래의 소스를 완벽하게 이해하는 것은 굉장히 어렵다.
'함수 포인터'를 먼저 이해하는 것이 필요하다. 하지만, 소스에 붙여진 설명으
로 '큰 그림(big picture)'을 보는 데에는 무리가 없을 것이다.

#include <dos.h>

#define INT_TIMER 0x8//타이머 인터럽트

#define INT_TSR   0x27//램 상주 인터럽트

#define FACE      1

#define NORMAL    7//검은 배경에, 회색 전경 속성

char far* video=(char far*)MK_FP(0xb800,0x0000);//비디오 램의 시작 주소

int oldx=40,oldy=12,dx=1,dy=1,i;

void interrupt (*oldtimer)(...);//함수 포인터 변수 선언

  //'함수 포인터'를 참고하라. interrupt는 이 함수가 ISR 임을 나타낸다.

void PutXY(int x,int y,unsigned char c,unsigned char attr) {

//ISR에서 ISR을 호출하는 것은 불법이므로, 문자를 찍기 위해

//이 함수를 호출해야 한다. 이 함수는 어떤 함수도 호출하지 않는다.

//즉, ISR을 사용하지 않는다.

    i=160*y+2*x;

    *(video+i)=c;

    *(video+i+1)=attr;

}//PutXY()

void interrupt NewTimer(...) {//ISR의 파라미터는 정해져 있지 않다.

// ...은 가변 인자 파라미터를 나타낸다. 또한, catch(...)에서처럼 모든

//예외를 잡기 위해 명시할 수 있다.

    (*oldtimer)();//③원래의 ISR을 호출한다.

    //④추가적인 일을 한다.

    PutXY(oldx,oldy,' ',0);//얼굴을 지운다.

    if (oldx==79 || oldx==0) dx=-dx;//가장 자리에서 진행 방향을 토글한다

    if (oldy==24 || oldy==0) dy=-dy;

    oldx+=dx; oldy+=dy;

    PutXY(oldx,oldy,FACE,NORMAL);//얼굴을 그린다.

}//NewTimer()

void TSR() {

    union REGS regs;

    regs.x.dx=4000;//램에 상주할 메모리의 크기를 바이트 단위로 명시한다.

    int86(INT_TSR,&regs,&regs);//램상주 인터럽트

}//TSR()

void main() {

    oldtimer=getvect(INT_TIMER);//①원래 주소를 얻는다.

    setvect(INT_TIMER,NewTimer);//②새 주소를 설치한다.

    TSR();

}//main()

  실행 결과는 아래 그림과 같다.

             프로그램을 실행하면, 아스키 1번 문자(얼굴)가 화면을 돌아 다닌다. 이
          프로그램은 자신을 램에 남겨 놓은 채, 종료하기 때문에, 다른 프로그램을
          실행할 수 있다. DOS에서 램상주는 멀티태스킹(multitasking)의 유일한 방법
          이었다.

  키워드 interrupt는 함수의 리턴 형과 이름 사이에 명시하여, 이 함수가 반
드시 인터럽트 서비스 함수로 사용되어야 함을 나타낸다(ISR의 프리픽스
(prefix)와 서픽스(suffix) 코드는 일반 함수의 것과 다르다). 또한,

void interrupt (*oldtimer)(...);

는 함수 선언이 아니라, 변수 선언이다. 즉, ...을 파라미터로 받고, void를 리
턴하는 인터럽트 서비스 루틴의 시작 주소를 가질 수 있는 '함수 포인터
(pointer to a function) 변수'를 선언한 것이다. 이것은 '함수 포인터'에서
자세히 다루므로, 여기서는 생략한다.

  필자는 한 번도 서브클래싱(subclassing)에 대해 다루지 않았다. 이것은
Windows 구조(architecture)와 관계가 있으므로, 필자의 다른 책 'MFC
Kernel'을 참고하라.

  연습문제

1. Windows 프로그래밍에서 서브클래싱(subclassing)에 대해서 상세히 설명
하라. 그것은 무엇을 가로채는 것인가?

2. sound()함수를 사용하여, '산토끼'를 램상주(RAM resident)로 연주하는 프
로그램을 작성하라.

3. 0x10은 비디오 인터럽트이다. 커서의 모양을 바꾸기 위해서, 화면 해상도를
바꾸기 위해서 등 대부분의 비디오 작업은 0x10번 인터럽트를 사용한다. 1개
의 인터럽트로 여러 가지 기능을 구현하는 것을 어떻게 구현할 수 있는가?

4.


24. 디버깅

24. 디버깅(debugging)

   벌레잡기의 유래

  옛날 옛적에(?) IBM에서 진공관을 사용하는 컴퓨터의 제작을 완료했다. 그
것이 MARK-I이었는지, ENIAC이었는지는 확실하지 않다(필자는 옛적에 어
떤 책에서 이 이야기를 읽었다). 하드웨어의 테스트가 모두 끝났지만, 어떤 동
작에서 결함이 발견되었다. IBM의 기술자는 이 문제를 두고 몇 일 밤을 고민
했다. 마침내 결함의 원인을 발견했는데, 원인은 바로 진공관사이에 끼여서 죽
어 있는 나방(moss)때문이었다. 이 나방을 제거하자 그 거대한 컴퓨터가 제대
로 동작했다고 한다. 이것이 유래가 되어 (하드웨어나) 소프트웨어의 논리적
오류(semantic error)를 수정하는 작업을 벌레잡기(debugging)라고 한다.

             진공관과 나방: 진공관 사이에 끼여진 나방이 진공관의 동작에 오류를 일
          으킨 것 같다.

  모든 컴파일러는 소프트웨어의 논리적 오류를 수정하기 위한 방법을 제공한
다. 논리적 오류(semantic error)는 컴파일시에는 발생하지 않는다. 그것은
실행 시 에러를 유발할 수도 있지만, 그렇지 않을 수도 있다. 이러한 잠재적인
에러를 수정하기(벌레를 잡기) 위해서는 아래의 기능들이 필요할 것이다.

  ① 추적(trace): 소스를 추적하는 기능

  ② 관찰(watch): 객체의 값을 관찰하는 기능

  ③ 중단점(break point): 디버깅 시점을 정하는 기능

  ④ 기타 기능: 객체의 값을 바꾸는 기능, 현재의 값으로 특정 식을 평가하는
기능 등

   필요한 설정

  소스의 디버깅 작업은 소프트웨어 인터럽트의 일종인 트랩(trap)으로 동작
하기 때문에, 컴파일러의 환경 설정을 해 주어야 한다. 아래의 그림은 볼런드
C++ 3.1과 비주얼 C++ 6.0의 경우 디버깅 모드로 설정을 하는 예이다.

  볼런드 C++ 3.1에서는 Option->Debuffer를 선택하여 Source Debugging 그
룹에서 On을 선택해야 한다.

             디버거 대화상자(Option->Debugger): 소스를 통합환경에서 디버깅하기 위
          해서는 Source Debugging 그룹의 On에 설정되어 있어야 한다. Standalone
          은 볼런드의 터보 디버거(Turbo debugger)를 사용할 것을 지정한다.

  비주얼 C++ 6.0의 경우 프로젝트 설정 대화상자(Alt+F7)의 Link 탭에서
Debug Info 체크 박스가 선택되어야 한다. 비주얼 C++는 프로젝트의 설정
(configuration)을 지원하는데, 빌드 타겟(build target)이 Debug로 설정되면
이 항목이 설정된다.

             비주얼 C++의 프로젝트 설정 대화상자(Alt+F7): Link 탭의 Category를 선
          택하면, 디버그 정보를 목적 파일(object file)에 포함할지를 설정할 수 있다.
          빌드 타겟(build target)이 Debug이면, 이 항목이 체크(check)된다.

추적(trace)

  추적은 소스를 한 줄 한 줄 실행하는 기능을 말한다. 이 때 현재 실행할 곳
을 가리키는 포인터를 실행 점(execution point)이라 한다. 실제로 가장 많
이 사용하는 기능이며, 실행해야 할 문장이 함수 호출인 경우, 함수의 내부까
지 추적하는 step into, 함수의 내부를 추적하지 않는 step over, 현재 실행
점에서 커서(cursor) 위치까지 수행하는 run to cursor 및 현재 실행 점이 함
수의 내부인 경우 함수를 탈출하는 step out등이 있다. 다음은 Borland C++
와 Visual C++에서 사용하는 키의 비교이다.

Borland C++ 3.1

Visual C++ 6.0

step into

step over

run to cursor

step out

F7

F8

F4

n/a

F11

F10

Ctrl+F10

Shift+F11

             추적에 해당하는 핫 키(hot key): 볼런드 C++ 3.1에서는 step out을 지원
          하지 않는다. 이것은 이후의 버전에서 개선되었다.

  위의 키를 반드시 외워두기 바란다. 물론 툴바나 메뉴를 이용하여 해당 기
능을 선택할 수 있다.

  아래의 그림들은 볼런드 C++ 3.1 및 비주얼 C++ 6.0의 해당하는 디버그 메
뉴이다.

             볼런드 C++ 3.1의 디버그 메뉴: 대부분의 디버깅에 해당하는 항목을 제공
          한다. 하지만, 추적에 해당하는 항목은 오로지 핫 키로만 사용이 가능하다.

             비주얼 C++ 6.0의 디버그 메뉴: 모든 디버깅 기능에 대해 메뉴 항목을 제
          공한다.

             비주얼 C++의 디버그 툴바: 자주 사용하는 대부분의 기능을 선택할 수
          있다.

관찰(watch) 및 중단점(break point)

  관찰(watch)은 특정한 변수나 객체의 현재 값을 관찰하는 기능이다. 변수
의 이름, 배열의 이름을 입력하거나, 객체의 이름을 입력하면 모든 내용을 관
찰 윈도우(watch window)에 보여준다.

  중단점(break point)은 프로그램을 실행하다가 디버깅을 시작할 위치를 지
정한다.

Borland C++ 3.1

Visual C++ 6.0

watch

break point

debug run

stop debugging

Ctrl+F7

Ctrl+F8

Ctrl+F9

Ctrl+F2

n/a

F9

F5

Shift+F5

             관찰과 중단점의 핫 키

  아래의 그림은 볼런드 및 비주얼 C++의 해당하는 관찰 윈도우, 중단점 및
실행 위치를 나타낸다.

             볼런드 C++ 3.1의 디버깅 모드: 실행 점, 중단 점 및 관찰 윈도우가 나타
          나 있다.

             비주얼 C++ 6.0의 디버깅 모드: 실행 점, 중단 점 및 관찰 윈도우가 나타
          나 있다.

기타 기능

  평가 및 변경(evaluate/modify)(현재 변수의 값을 임의의 값으로 바꾸는 기
능) 및 점검(inspect)(사용자가 입력한 식을 현재의 변수 값으로 평가하는 기
능)을 제공하며, 기타 스택 내용보기(watch stack) 등의 고급 내용도 지원한
다.

Borland C++ 3.1

Visual C++ 6.0

evaluate/modify

inspect

Ctrl+F4

Alt+F4

n/a

n/a

             기타 기능의 핫 키: GUI를 지원하는 비주얼 C++에서는 고급 기능에 대해
          서 핫 키를 제공하지 않는다.

   예제

  자 아래의 프로그램에 대해 논리적인 에러를 잡아 보자. 아래의 프로그램은
1에서 10까지의 합과 곱을 출력하는 프로그램이다.

#include <stdio.h>

long Fact(long n) {

    long product=0,i;

    for (i=1;i<=n;++i)

        product=product*i;

    return product;

}

void main() {

    long s=0,i;

    for (i=1;i<=10;++i)

        s=s+i;

    printf("sum of 1 to 10 is %ld\n",s);

    s=Fact(10);

    printf("10 factorial is %ld\n",s);

}

  실행 결과는

    sum of 1 to 10 is 55

    10 factoral is 0

이다. 합은 제대로 출력된 것 같지만, 곱을 계산하는 부분은 분명히 논리적인
에러이다. 자 Borland C++에서 디버거를 실행하자(Visual C++로도 꼭 실행해
보기 바란다)!

  소스에서 보면 합을 구하는 부분은 확실하게 에러가 없는 것 같다. 그러면,
계승(factorial)을 구하는 부분부터 추적해 보도록 하자. 커서를

s=Fact(10);

에 위치시킨 다음 run to cursor(F4)를 선택한다.

             F4를 눌러 원하지 않는 부분의 추적을 피할 수 있다.

  실행점이 s=Fact(10); 에 멈추었다. 실행 점은 아직 실행되지 않았다. 여기
서 s의 값과 i의 값을 관찰하기 위해 add watch(Ctrl+F7)를 선택하자. 여기서
main()의 지역 변수인 s와 i를 입력한다.

             Ctrl+F7을 누르면 변수, 배열 혹은 객체의 값을 관찰할 수 있도록 명칭
          (identifier)을 입력할 수 있다.

  Watch Expression 대화상자에서 s와 i를 각각 입력하면, 변수가 watch 윈
도우에 추가된다. s의 값이 55인 것과 i의 값이 11인 것을 확인하자.

             함수의 내부까지 추적하기 위해서는 F7을 눌러야 한다. F8을 누르면 함
          수를 실행하지만 추적하지는 않는다.

  이제 Fact(10) 함수의 내부를 추적해야 하므로, step into(F7)를 선택한다.
주의하라! step over(F8)를 선택하면 함수의 내부는 추적하지 않는다. 아직 함
수의 내부에 진입하지 않았으므로, s와 i값은 정의되지 않았다고 나타난다.

  다시 Ctrl+F7을 눌러, n과 product를 watch 윈도우에 추가하자. Add Watch
대화상자에서 n을 입력할 때

n,x

라고 입력하면, n의 값은 16진수로 나타난다. 문자 및 십진수로 표시하기 위
해 x 대신 c,d를 사용할 수도 있다. i가 2가 될 때까지 F7을 눌러 추적해 보
자.

             F7을 눌러 Watch 윈도우에 있는 변수의 값을 조심스럽게 관찰한다.

  product의 값이 0으로 고정된 채 갱신되지 않는다. 이런! product의 값은 0
이 아닌 1로 초기화해야 한다. 자

long product=0,i;

를

long product=1,i;

로 수정하자. 디버깅을 취소(Ctrl+F2)한 다음,

return product;

에 커서를 위치시킨 다음 Ctrl+F8을 눌러 중단점을 설정하자. 그리고 Ctrl+F9
를 눌러 실행한다. 이 문장이 실행되기 전 디버깅 모드로 전환된다.

             특정한 부분에서 디버깅을 하고 싶으면, 디버깅을 원하는 줄에서 중단점
          을 설정(Ctrl+F8)한 다음 실행(Ctrl+F9)하면 된다.

  Watch 윈도우의 product 값을 보면 이제 제대로 계산된 것 같다. Ctrl+F2
를 눌러 디버깅을 중단하고, 다시 Ctrl+F8을 눌러 중단점을 모두 해제하자.

   연습문제

1. 위의 예제에 대해 비주얼 C++에서 추적해 보라.

2. Evaluate/Modify의 기능이 언제 필요한가? 구체적인 예를 들라.

3.


25. 표준함수

25. 표준 함수(standard
function)

  이 장에서 이야기하는 함수들은 ANSI1) 표준 함수가 대부분이다. 여러분이
  1) 미국 국제 표준화 기구(American National Standard Institute)로 각종 표준들을

    제정(establish)하고 권고(recommend)한다.

얼마나 많은 함수의 역할을 알고 있는지 점검하라. 먼저 이 장의 첫 부분은
Borland C++ 4.5의 도움말을 참고로 하여, DOS 전용 함수2)와 Win95의

  2) 즉 DOS 전용 함수(Win16이상에서 사용하지 못하는 함수)는 열거하지 않는다. 물론

    Win16과 DOS에서 사용 가능한 함수는 열거한다.

API함수를 제외한 함수 모두를 열거하였다. 함수의 선언이 들어있는 헤더 파
일(header file)을 함수 이름 이후의 괄호에 명시하였으며, 함수의 원형
(prototype)은 명시하지 않았다. 이 장의 마지막 부분에서는 ANSI 표준 함수
가 아닌 것 중에, Borland의 고유한 함수에 대응하는 Microsoft의 함수를 표
에 제시하였다. Microsoft의 함수는 Visual C++ 4.2의 도움말을 참고로 하였
다.

  이 장의 마지막 부분에서는 Borland와 Microsoft의 컴파일러에서 모두 동
작하는 프로그램을 작성하는 방법에 대해서 설명하고 예를 들었다. 함수의 설
명은 간단하게 기술하였다. 자세한 사항은 각 컴파일러의 라이브러리 함수 도
움말을 이용하라.

  분류 함수(Classification Routines)

  아래의 함수들은 ASCII 문자(character)의 종류를 구하는데 유용하게 사용
할 수 있다.

isalnum(ctype.h)

    Is AlphaNumeric Character; 알파벳 혹은 숫자 문자3)인지를 검사

  3) ASCII 48번에서 57번까지의 문자, '0', '1', ... ,'9'를 말한다.

    한다.

isalpha(ctype.h)

    Is Alphabet Character; 알파벳 문자인지를 검사한다.

isascii(ctype.h)

    Is ASCII Character; 아스키 문자인지를 검사한다.

iscntrl(ctype.h)

    Is Control Character; 컨트롤 문자(0-31)인지를 검사한다.

isdigit(ctype.h)

    Is Digit Character; 숫자 문자인지를 검사한다.

isgraph(ctype.h)

    Is Graphic Character; 아스키 그래픽 문자인지를 검사한다.

islower(ctype.h)

    Is Lower Character; 소문자인지를 검사한다.

isprint(ctype.h)

    Is Printable Character; 프린트 가능한 문자인지를 검사한다.

ispunct(ctype.h)

    Is Punctuation Character; 구두점 문자인지를 검사한다.

isspace(ctype.h)

    Is White Space4) Character; space, tab, carriage return, new line,

  4) 흰 공백(white space)은 컴파일러가 컴파일 과정에서 토큰과 토큰의 분리자

    (separator)로써 무시하는 문자를 의미한다. 일반적으로 텍스트 에디터 상에서는 사용

    자가 입력할 수 있는, space, enter(carriage return+line feed), tab을 의미한다.

    vertical tab, formfeed (0x09 to 0x0D, 0x20)인지를 검사한다.

isupper(ctype.h)

    Is Upper Character; 대문자인지를 검사한다.

isxdigit(ctype.h)

    Is HexaDigit; 16진 문자인지를 검사한다.

  콘솔 입출력 함수(Console5) I/O Routines)

  5) 콘솔(console)은 고전적으로 표준 입출력장치(standard I/O device)를 의미한다. 개인

    용 컴퓨터가 일반화되기 전에, 사람들은 중앙 컴퓨터에 연결된 단말기(terminal) 장치를

    사용했는데, 이것에 붙인 고유 이름이 콘솔이었다. 즉, 콘솔은 화면(screen)과 키보드가

    붙은 단말기 장치를 의미한다. 그러므로 '콘솔 입출력=화면 출력과 키보드 입력=표

    준 입출력'을 의미하게 되었다.

cgets6)(conio.h)

  6) gets는 stdio.h에 지정된 ANSI 표준 함수인 반면에, cgets는 gets와 역할이 같지만,

    표준 함수가 아니라는 점에 주목하라. 입출력 함수들 중, 표준 함수가 아닌 모든 콘솔

    입출력 함수는 conio.h에 지정되어 있다. 이것은 IBM의 콘솔 기능(색,윈도우 등)이 업

    계의 표준이 아니기 때문이다. 즉 원래의 콘솔에는 글자의 색을 지정할 수 없었다. 하

    지만, IBM 의 컬러 모니터에는 글자의 색 지정이 가능하다. 그러므로 색을 지정하는

    함수나 색이 있는 문자열을 입력 받는 cgets 같은 함수를 ANSI에서 표준으로 정할 수

    없었다. 하지만, 대부분의 상용 컴파일러에서는 conio.h를 포함하고 있다. 이것이 printf

    와 cprintf가 stdio.h와 conio.h에 각각 분리되어 있는 이유이다. printf로는 색이 있는 문

    자열을 출력할 수 없다.

    Console Get String; 문자열을 입력 받는다.

clreol(conio.h)

    Clear To End Of Line; 줄의 끝까지 지운다.

clrscr(conio.h)

    Clear Screen; 화면을 현재의 바탕색으로 모두 지운다.

cprintf(conio.h)

    Console Print Formatted String; 형식화된 문자열을 출력한다.

cputs(conio.h)

    Console Put String; 문자열을 출력한다.

delline(conio.h)

    Delete Line; 한 줄을 지운다.

getpass(conio.h)

    Get Password; 비밀번호를 입력 받는다.

gettext(conio.h)

    Get Text; 화면의 일부를 메모리로 복사한다.

gettextinfo(conio.h)

    Get Text Mode Information; 텍스트 모드의 비디오 정보를 얻는다.

gotoxy(conio.h)

    Goto (X,Y); 커서를 특정한 곳에 위치시킨다.

highvideo(conio.h)

    High-Intensity Video; 밝기가 높은 문자로 설정한다.

insline(conio.h)

    Insert Line; 한 줄을 입력한다.

lowvideo(conio.h)

    Low-Intensity Video; 밝기가 낮은 문자로 설정한다.

movetext(conio.h)

    Move Text; 화면에 있는 텍스트를 옮긴다.

normvideo(conio.h)

    Normal Video; 정상 상태의 문자로 설정한다.

putch(conio.h)

    Put Character; 문자를 출력한다.

puttext(conio.h)

    Put Text; 메모리에 복사된 문자들을 화면에 출력한다.

_setcursortype(conio.h)7)

  7) 함수의 이름 앞에 언더스코어(underscore: _)가 있는 함수는 표준함수가 아니다는 것

    을 컴파일러 제조 업체(vendor)가 강조한 것이다. 미리 정의된 매크로나 전역 변수 대

    부분은 한 개 혹은 두 개의 언더스코어를 포함하고 있는데, 이것은 표준이 아니며 또

    한 사용자가 정의하는 전역 변수와의 충돌을 피하기 위해 붙여졌다.

    커서의 모양을 설정한다.

textattr(conio.h)

    Text Attribute; 문자의 속성을 설정한다.

textbackground(conio.h)

    문자의 배경색을 설정한다.

textcolor(conio.h)

    문자의 전경색(foreground color)을 설정한다.

textmode(conio.h)

    텍스트 모드를 설정한다.

ungetc(stdio.h)

    입력 스트림(stream8))에 문자를 거꾸로 갖다 놓는다.

  8) 스트림은 데이터의 연속적인 흐름(stream)을 의미한다. 예를 들면, 키보드에서 연속

    적인 데이터의 입력이 일어 날 수 있으므로, 키보드는 입력 스트림이다. 일반적으로

    스트림은 파일(file)과 같은 개념이다. 운영체제는 키보드를 표준 입력 파일로 취급한

    다. 파일은 사용 전에 열어야 하는데, 운영체제가 부팅(booting)할 때, 표준 입출력 파

    일은 자동으로 열린다.

wherex(conio.h)

    커서의 x위치를 구한다.

wherey(conio.h)

    커서의 y위치를 구한다.

window(conio.h)

    텍스트 윈도우를 생성한다.

  변환 함수(Conversion Routines)

atof(stdlib.h)

    ASCII string to Float; 문자열을 부동소수형으로 바꾼다.

atoi(stdlib.h)

    ASCII string to Integer; 문자열을 정수형으로 바꾼다.

atol(stdlib.h)

    ASCII string to long integer; 문자열을 긴 정수형으로 바꾼다.

ecvt(stdlib.h)

    Exponential Convert to string; 지수형의 실수를 문자열로 바꾼다.

fcvt(stdlib.h)

    Floating Convert; 실수를 문자열로 바꾼다.

gcvt(stdlib.h)

    General Convert; 일반형의 실수를 문자열로 바꾼다.

itoa(stdlib.h)

    Integer to ASCII; 정수를 문자열로 바꾼다.

ltoa(stdlib.h)

    Long integer to ASCII; 긴 정수를 문자열로 바꾼다.

strtod(stdlib.h)

    String to Double; 스트링을 더블형으로 바꾼다.

strtol(stdlib.h)

    String to Long; 스트링을 긴 형으로 바꾼다.

_strtold(stdlib.h)

    String to Long Double; 스트링을 긴 더블형으로 바꾼다.

strtoul(stdlib.h)

    String to Unsigned Long; 스트링을 부호 없는 긴 형으로 바꾼다.

toascii(ctype.h)

    To ASCII; 문자를 아스키 문자로 바꾼다.

_tolower(ctype.h)

    To Lower Character; 소문자로 바꾼다.

tolower(ctype.h)9)

  9) 함수 이름은 같지만, 언더스코어가 붙어 있는 함수가 있는 경우가 있다. 이 때 언더

    스코어가 붙은 함수는 더 하위 레벨(low level)의 함수이다. 함수가 하위 레벨이라는 의

    미는 일반적으로 더 자세하게 다룰 수 있으며, 속도가 더 빠르지만, 기계에 의존적이

    라는 의미이다.

    소문자로 바꾼다.

_toupper(ctype.h)

    To Upper Character; 대문자로 바꾼다.

toupper(ctype.h)

    대문자로 바꾼다.

ultoa(stdlib.h)

    Unsigned Long to ASCII; 부호 없는 긴 형을 아스키 문자열로 바꾼

    다.

  진단 함수(Diagnostic Routines)

assert(assert.h)

    Assert Condition; 조건을 비교하여 가능하면 종료한다.

_matherr(math.h)

    Math Error; 사용자가 변경할 수 있는 수학 에러 핸들러(handler)이

    다.

_matherrl(math.h)

    Math Error; 사용자가 변경할 수 있는 수학 에러 핸들러(handler)이

    다.

perror(errno.h)

    Print System Error Message; 시스템 에러 메시지를 출력한다.

  폴더(folder10)) 제어 함수(Directory Control
Routines)

  10) 폴더와 디렉토리는 같은 개념이다. 명령 행(command line) 프람프트(prompt) - 예를

    들면 C:\> - 에서 사용하던, 디렉토리와 구분하기 위해서, 일반적으로 GUI 환경에서는

    폴더란 말을 사용한다.

chdir(dir.h)

    Change Directory; 현재 디렉토리를 바꾼다.

_chdrive(direct.h)

    Change Drive; 현재 드라이브를 바꾼다.

closedir(direct.h)

    Close Directory; 디렉토리 스트림을 닫는다.

_dos_findfirst(dos.h)11)

  11) 이 함수의 옛날 이름은 findfirst였다. 도스와 Win95는 파일 시스템이 다르므로,

    Win95에서 findfirst는 Win95의 함수로 승격되었다. 도스에서 findfirst는 이 함수를 사용

    해야 한다. 이러한 함수가 이 밖에도 몇 개 더 존재한다. 어떤 함수는 아예 폐기

    (obsolete) 되었다.

    Find Fist File; 첫 번째 파일을 찾는다.

_dos_findnext(dos.h)

    Find Next File; 연속된 다음의 파일을 찾는다.

_dos_getdiskfree(dos.h)

    Get Disk Free Space; 디스크의 남은 공간을 구한다.

_dos_getdrive(dos.h)

    Get Drive; 현재 드라이브를 구한다.

_dos_setdrive(dos.h)

    Set Drive; 현재 드라이브를 정한다.

findfirst(dir.h)

    Find First File; 첫 번째 파일을 찾는다.

findnext(dir.h)

    Find Next File;  연속된 다음의 파일을 찾는다.

fnmerge(dir.h)

    Filename Merge; 파일 이름을 합친다.

fnsplit(dir.h)

    Filename Split; 파일 이름을 분리한다.

_fullpath(stdlib.h)

    Full Path; 완전한 경로를 구한다.

getcurdir(dir.h)

    Get Current Directory; 현재 디렉토리를 구한다.

getcwd(dir.h)

    Get Current Working Directory; 현재 작업 디렉토리를 구한다.

_getdcwd(direct.h)

    Get Drive Specified Current Working Directory; 지정된 드라이브의

    현재 작업 디렉토리를 구한다.

getdisk(dir.h)

    현재의 디스크를 구한다.

_makepath(stdlib.h)

    부분품에서 경로를 구성한다.

mkdir(dir.h)

    Make Directory; 디렉토리를 만든다.

mktemp(dir.h)

    Make Temp; 유일한(unique) 파일 이름을 만든다12).

  12) 현재의 디렉토리에 없는 유일한 파일을 생성할 필요가 종종 발생한다. 이 때 이 함

    수를 사용한다. 후에 사용자가 명시적으로 지워주지 않으면, 디스크에 남게 된다.

opendir(direct.h)

    Open Directory Stream; 읽을 목적으로 디렉토리 스트림을 연다.

readdir(dirent.h)

    Read Current Directory Entry; 디렉토리 스트림에서 현재 시점

    (entry)를 읽는다.

rewinddir(dirent.h)

    Rewind Directory; 디렉토리 스트림의 현재 시점을 처음으로 되돌린

    다.

rmdir(dir.h)

    Remove Directory; 디렉토리를 제거한다.

_searchenv(stdlib.h)

    Search Environment; 파일을 환경에서 설정한 경로에서 찾는다.

searchpath(dir.h)

    파일의 운영체제의 탐색 경로를 찾는다.

_searchstr(stdlib.h)

    Search String; 파일을 위해서 디렉토리의 리스트를 찾는다.

setdisk(dir.h)

    현재의 드라이브 번호를 설정한다.

_splitpath(stdlib.h)

    경로 이름을 분리한다.

  윈도우 함수(EasyWin Routines)

clreol(conio.h)

    Clear to End Of Line; 줄의 끝까지 지운다.

clrscr(conio.h)

    Clear Screen; 화면을 현재의 배경색으로 지운다.

fgetchar(stdio.h)

    File Get Character; 파일에서 한 문자를 읽는다.

getch(conio.h)

    Get Character; 한 문자를 읽는다.

getchar(stdio.h)

    Get Character; 한 문자를 읽는다13).

  13) getchar은 표준 함수이지만, getch는 conio.h에 정의되어 있는 표준이 아닌 함수이

    다. 하지만, 일반적으로 conio.h는 대부분의 컴파일러에서 지원하므로, getchar보다는

    getch를 많이 쓴다. 이것은 getch가 enter로 문자의 끝을 나타낼 필요가 없는 때문이기

    도 하다.

getche(stdio.h)

    Get Character and Echo14); 문자를 입력받고 화면에 출력한다.

  14) 사용자가 입력한 문자가 콘솔 스크린에 나타나는 것을 에코(echo)라고 한다.

gets(stdio.h)

    Get String; 스트링을 입력 받는다.

gotoxy(conio.h)

    Goto (x,y); 커서를 특정 위치로 옮긴다.

kbhit(conio.h)

    Keyboard Hit; 키보드가 눌려졌는지 검사한다.

perror(errno.h)

    Print Error; 에러 메시지를 출력한다.

printf(stdio.h)

    Print Formatted String; 형식화된 문자열을 출력한다.

putch(conio.h)

    Put Character; 문자를 출력한다.

putchar(stdio.h)

    Put Character; 문자를 출력한다.

puts(stdio.h)

    Put String; 문자열을 출력한다.

scanf(stdio.h)

    Scan from Formatted String; 형식화된 문자열에서 입력 받는다.

vprintf(stdio.h)

    Video Print Formatted String; 형식화된 문자열을 표준 출력 장치인

    화면에 출력한다.

vscanf(stdio.h)

    형식화된 문자열에서 표준 입력 장치에서 입력 받는다.

wherex(conio.h)

    Where x; 커서의 x위치를 얻는다.

wherey(conio.h)

    Where y; 커서의 y위치를 얻는다.

  인라인 함수(Inline Routines)

abs(math.h)

    Absolute value; 정수의 절대값을 구한다.

alloca(malloc.h)

    Allocation; 임시 스택 공간을 할당한다.

_crotl(stdlib.h)

    Count Rotate Left; 왼쪽으로 rotate한다.

_crotr(stdlib.h)

    Count Rotate Right; 오른쪽으로 rotate한다.

_lrotl(stdlib.h)

    Long Rotate Left; unsigned long을 왼쪽으로 rotate한다.

_lrotr(stdlib.h)

    Long Rotate Right; unsigned long을 오른쪽으로 rotate한다.

memchr(mem.h)

    Search Memory Character; 메모리에서 특정한 문자를 검색한다.

memcmp(mem.h)

    Memory Compare; 두 개의 메모리 블락을 비교한다.

memcpy(mem.h)

    Memory Copy; 메모리 블락을 복사한다.

memset(mem.h)

    Memory Set; 메모리를 특정 문자로 설정한다.

_rotl(stdlib.h)

    Rotate Left; 왼쪽으로 rotate한다.

_rotr(stdlib.h)

    Rotate Right; 오른쪽으로 rotate한다.

stpcpy(string.h)

    String Copy; 스트링을 복사한다.

strcat(string.h)

    String Concatenation; 스트링을 접속한다.

strchr(string.h)

    String Character search; 문자열에서 문자를 찾는다.

strcmp(string.h)

    String Compare; 문자열을 비교한다.

strcpy(string.h)

    String Copy; 문자열을 복사한다.

strlen(string.h)

    String Length; 문자열의 길이를 구한다.

strncat(string.h)

    String Number Concatenation; 스트링의 제한된 길이 만큼을 접속한

    다.

strncmp(string.h)

    String Number Compare; 스트링의 제한된 길이 만큼을 비교한다.

strncpy(string.h)

    String Number Copy; 스트링의 제한된 길이 만큼을 복사한다.

strnset(string.h)

    String Number Set; 스트링의 제한된 길이 만큼을 설정한다.

strrchr(string.h)

    String Reverse Character; 스트링에서 주어진 마지막 문자를 찾는

    다.

strset(string.h)

    String Set; 스트링을 특정한 문자로 설정한다.

  입출력 함수(Input/output Routines)

access(io.h)

    Is file accessable?; 파일의 접근 가능성을 검사한다.

chmod(io.h)

    Change Mode; 파일의 접근 모드를 변경한다.

chsize(io.h)

    Change Size; 파일의 크기를 변경한다.

clearerr(stdio.h)

    Clear Error; 에러 상태를 초기화한다.

close(io.h)

    Close file; 파일을 닫는다.

creat(io.h)

    Creat file; 새로운 파일을 만들거나, 있으면 덮쳐서 만든다.

creatnew(io.h)

    Creat New file; 새로운 파일을 만든다.

creattemp(io.h)

    Creat Temporary file; 특정한 디렉토리에서 유일한 이름을 가지는

    파일을 만든다.

cscanf(conio.h)

    Console Scan from Formatted string; 표준 입력 장치에서 문자열의

    형식대로 입력 받는다.

_dos_close(dos.h)

    파일을 닫는다. DOS에서만 사용 가능하다15).

  15) Win95이전의 컴파일러에서 지원하던 _close는 Win95로 승격되었다. 그래서, 도스용

    의 _close와의 호환을 위해 _dos_close등의 함수가 추가되었다. _dos로 시작하는 함수

    는 모두 이러한 경우이다.

_pclose(stdio.h)

    Pipe Close; 파이프된 명령이 완료될 때까지 기다린다.

_dos_creat(dos.h)

    파일을 만든다. DOS에서만 사용 가능하다.

_dos_creatnew(dos.h)

    새로운 파일을 만든다.

_dos_getfileattr(dos.h)

    DOS Get File Attribute; 파일의 속성을 얻는다.

_dos_getftime(dos.h)

    DOS Get File Timestamp; 파일의 시간 정보를 얻는다.

_dos_open(dos.h)

    파일을 연다.

_dos_read(dos.h)

    파일에서 읽는다.

_dos_setfileattr(dos.h)

    DOS Set File Attribute; 파일의 속성을 설정한다.

_dos_setftime(dos.h)

    DOS Set File Timestamp; 파일의 시간을 설정한다.

_dos_write(dos.h)

    파일에 쓴다.

dup(io.h)

    Duplicate File Handle; 파일 핸들(handle16))을 복사한다.

  16) 핸들(handle)은 운영체제에서 사용하는 일반적인 용어인데, 특정 메모리 블락의 유

    일한 식별 번호를 의미한다. 예를 들면, 프로세스가 메모리에서 여러 개 실행되고 있

    다면, 운영체제는 이러한 프로세스들을 구분하기 위해서, 프로세스 제어 블락

    (process control block; PCB)을 유지하는데 이러한 블락에는 고유한 정수값(integer

    value)이 주어진다. 이 때 이 정수값을 프로세스 핸들이라 한다. 만약 그 블락이 파일

    제어 블락(file control block; FCB)이라면, 파일 핸들, 메모리 제어 블락이라면, 메모리

    핸들이라고 한다. 이것은 Win95 프로그래머에게 익숙한 개념이다.

dup2(io.h)

    파일 핸들을 이미 존재하는 파일 핸들로 복사한다.

eof(io.h)

    End Of File; 파일의 끝인지를 검사한다.

fclose(stdio.h)

    File Close; 파일을 닫는다.

fcloseall(stdio.h)

    File Close All; 파일을 모두 닫는다.

fdopen(stdio.h)

    File Open; 스트림을 파일 핸들과 연관시킨다.

feof(stdio.h)

    File End Of File; 파일의 끝인지를 검사한다.

ferror(stdio.h)

    File Error; 파일의 에러를 찾는다.

fflush(stdio.h)

    File Flush; 파일 입출력에 사용된 버퍼를 플러쉬(flush17))한다.

  17) 플러쉬는 버퍼 입출력을 했을 때, 아직 버퍼에 남아 있지만, 파일에 쓰여지지 않은

    정보를 실제 파일에 쓰는 작업을 말한다. 버퍼의 내용이 흘러 나온다(flush)는 의미이

    다.

fgetc(stdio.h)

    File Get Character; 파일에서 문자를 읽는다.

fgetchar(stdio.h)

    File Get Character; 파일에서 문자를 읽는다.

fgetpos(stdio.h)

    File Get Position; 파일 포인터(file pointer)의 위치를 얻는다.

fgets(stdio.h)

    File Get String; 파일에서 문자열을 읽는다.

filelength(io.h)

    File Length; 파일의 크기를 얻는다.

fileno(stdio.h)

    File No.; 파일 핸들 번호를 얻는다.

flushall(stdio.h)

    Flush All; 모든 스트림을 플러쉬한다.

fopen(stdio.h)

    File Open; 파일을 연다.

fprintf(stdio.h)

    File Print Formatted String; 파일에 형식화된 문자열을 출력한다.

fputc(stdio.h)

    File Put Character; 파일에 문자를 출력한다.

fputchar(stdio.h)

    File Put Character; 파일에 문자를 출력한다.

fputs(stdio.h)

    File Put String; 파일에 문자열을 출력한다.

fread(stdio.h)

    File Read; 파일로부터 읽는다.

freopen(stdio.h)

    File Reopen; 파일을 다시 연다.

fscanf(stdio.h)

    File Scan from Formatted string; 파일에서 문자열의 형식대로 읽는

    다.

fseek(stdio.h)

    File Seek; 파일 포인터를 이동한다.

fsetpos(stdio.h)

    File Set Position; 파일 포인터를 설정한다.

_fsopen(stdio.h)

    File Sharing Open; 파일을 공유할 목적으로 연다.

fstat(sys\stat.h)

    File Statistics; 파일의 정보(information)를 얻는다.

ftell(stdio.h)

    File Pointer Tell; 현재의 파일 포인터를 얻는다.

fwrite(stdio.h)

    File Write; 파일에 블락을 쓴다.

getc(stdio.h)

    Get Character; 스트림에서 문자를 읽는다.

getch(conio.h)

    Get Character; 표준 입력 장치(키보드)에서 문자를 입력 받는다.

getchar(stdio.h)

    Get Character; 문자를 입력 받는다.

getche(conio.h)

    Get Character and Echo; 문자를 입력 받고 에코한다.

getftime(io.h)

    Get File Timestamp; 파일의 시간 정보를 얻는다.

gets(stdio.h)

    Get String; 스트링을 입력 받는다.

getw(stdio.h)

    Get Word; 스트림에서 정수를 입력 받는다.

ioctl(io.h)

    IO Control; IO장치를 제어한다.

isatty(io.h)

    Is Auxilary Terminal Type; 장치 형을 검사한다.

kbhit(conio.h)

    Keyboard Hit; 키보드의 임의의 키가 눌린지 검사한다.

lock(io.h)

    Lock; 파일 공유 락을 설정한다.

locking(io.h)

    Locking; 파일 공유 락을 설정하거나 해제한다.

lseek(io.h)

    Long Seek; 파일 포인터를 이동시킨다.

open(io.h)

    Open File; 파일을 연다.

_open_osfhandle(io.h)

    Open OS File Handle; 파일 핸들을 연관(associate)시킨다.

perror(stdio.h)

    Print Error; 시스템 에러 메시지를 출력한다.

_pipe(io.h)

    Pipe; 읽기/쓰기 파이프를 생성한다.

_popen(stdio.h)

    Processor Open; 명령 프로세스 파이프를 만든다.

printf(stdio.h)

    Print Formatted String; 형식화된 문자열을 출력한다.

putc(stdio.h)

    Put Character; 스트림에 문자를 출력한다.

putchar(stdio.h)

    Put Character; 문자를 출력한다.

puts(stdio.h)

    Put String; 문자열을 출력한다.

putw(stdio.h)

    Put Word; 스트림에 정수를 출력한다.

read(io.h)

    파일에서 읽는다.

remove(stdio.h)

    파일을 제거한다.

rename(stdio.h)

    파일의 이름을 바꾼다.

rewind(stdio.h)

    파일 포인터를 처음으로 돌린다.

rmtmp(stdio.h)

    Remove Temporary file; 임시 파일을 지운다.

_rtl18)_chmod(io.h)

  18) 앞에 _rtl이 붙은 함수는 이전 컴파일러에서 사용하던 모호한(obsolete) 함수와 구분

    하기 위해 붙여진 이름이다.

    Run Time Library Change Mode; 파일의 모드를 바꾼다.

_rtl_close(io.h)

    파일을 닫는다.

_rtl_creat(io.h)

    파일을 생성한다.

_rtl_open(io.h)

    파일을 연다.

_rtl_read(io.h)

    파일에서 읽는다.

_rtl_write(io.h)

    파일에 쓴다.

scanf(stdio.h)

    Scan from Formatted string; 문자열의 형식대로 입력 받는다.

setbuf(stdio.h)

    Set Buffer; 스트림에 버퍼링 기능을 할당한다.

setftime(io.h)

    Set File Timestamp; 파일의 날짜를 설정한다.

setmode(io.h)

    Set Mode; 파일의 모드를 설정한다.

setvbuf(stdio.h)

    Set Variable length Buffer; 스트림에 버퍼 기능을 할당한다.

sopen(io.h)

    Shared file Open; 공유 파일을 연다.

sprintf(stdio.h)

    String Print Formatted string; 포맷된 문자열을 문자열에 출력한다.

sscanf(stdio.h)

    String Scan from Formatted string; 문자열에서 입력 받는다.

strerror(stdio.h)

    String Error; 에러 메시지 스트링의 시작 주소를 리턴한다.

_strerror(string.h, stdio.h)

    String Error; 고객화(customize)된 에러 메시지를 만든다.

tell(io.h)

    Tell; 파일 포인터의 현재 위치를 얻는다.

tempnam(stdio.h)

    Temporary Name; 지정한 디렉토리에 유일한 파일 이름을 만든다.

tmpfile(stdio.h)

    Temporary File; 바이너리 모드로 스크랫치(scratch19)) 파일을 연

  19) 스크랫치(scratch)는 Win95에서 응용 프로그램 사이에서 데이터를 교환하는 방법

    이다. 클립보드(clipboard)와는 다르게 데이터가 파일에 기록된다. 물론, 파일의 복사에

    관한 정보도 파일의 헤더에 기록된다. 지금 Win95에서 워드패드(WordPad)의 두 개의

    인스턴스를 실행하라. 첫 번째 워드 패드에서 내용을 입력한 다음 일부를 블락 선택

    하여, 마우스로 바탕화면(desktop)에 끌어다 놓기(drag & drop)하라. 스크랫치 파일이

    만들어질 것이다. 이것을 다른 워드패드에 끌어다 놓기하면, 내용이 복사된다.

    다.

tmpnam(stdio.h)

    Temporary Name; 유일한 파일 이름을 생성한다.

umask(io.h)

    파일의 읽기/쓰기 허가 마스크(permission mask)를 설정한다.

unlink(dos.h)

    파일을 지운다.

unlock(io.h)

    파일 공유 락을 해제한다.

utime(utime.h)

    Used Time; 파일의 시간과 날짜를 설정한다.

v20)fprintf(stdio.h)

  20) 접두문자 v는 가변 인자 리스트(variable argument list)를 의미한다. 각 컴파일러의

    도움말을 참고하라.

    Variable argument File Printf; 포맷된 문자열을 스트림에 출

    력한다. 단, 아규먼트(argument) 리스트를 받지 않고, 리스트 아규먼

    트(list of argument)를 받는다.

vfscanf(stdio.h)

    Variable argument File Scanf; 포맷된 문자열의 형식대로 스트림에

    서 입력 받는다.

vprintf(stdio.h)

    포맷된 문자열을 출력한다.

vscanf(stdio.h)

    포맷된 문자열의 형식대로 입력 받는다.

vsprintf(stdio.h)

    Variable argument String Printf; 포맷된 문자열을 스트링에 출력한

    다.

vsscanf(io.h)

    Variable argument String Scanf; 포맷된 문자열의 형식대로 문자열

    에서 입력 받는다.

  인터페이스 함수(Interface Routines)

bdos(dos.h)

    도스 시스템 콜을 접근한다.

bdosptr(dos.h)

    Base DOS Pointer; 도스 시스템 콜을 접근한다.

_bios21)_equiplist(bios.h)

  21) BIOS는 PC의 ROM에 있는 기본 입출력 서비스(Basic Input Output Service)를 의미

    한다. PC의 ROM에는 전원이 켜졌을 때, 동작하기 위한 기본 입출력 루틴을 가지고

    있다. 이 밖에도 POST(Power On Self Tester)와 Bootstrap Loader가 들어있다.

    BIOS Equipment List; 장비를 검사한다.

biosequip(bios.h)

    장비를 검사한다.

biosmemory(bios.h)

    메모리의 크기를 리턴한다.

biostime(bios.h)

    바이오스 시간을 읽거나 설정한다.

_chain_intr(dos.h)

    Chain Interrupt; 다른 인터럽트 핸들러(interrupt handler)와 체인

    (chain)한다.

country(dos.h)

    나라에 의존적인 정보를 리턴한다.

ctrlbrk(dos.h)

    Control Break; 컨트롤 브레이크 핸들러를 설정한다.

_disable(dos.h)

    인터럽트를 가능/불가능하게 만든다.

disable(dos.h)

    인터럽트를 가능/불가능하게 만든다.

dosexterr(dos.h)

    DOS Extended Error; 확장된 도스 에러 정보를 얻는다.

_dos_getvect(dos.h)

    DOS Get interrupt Vector; 인터럽트 벡터를 얻는다.

_dos_setvect(dos.h)

    DOS Set interrupt Vector; 인터럽트 벡터를 설정한다.

_enable(dos.h)

    인터럽트를 가능/불가능하게 만든다.

enable(dos.h)

    인터럽트를 가능/불가능하게 만든다.

FP_OFF22)(dos.h)

  22) 함수 이름이 모두 대문자인 함수는 매크로 함수(macro function)이다. 이것은 관례

    (convention)이다.

    Far Pointer23) Offset; 원거리 포인터의 변위(offset)를 얻는다.

  23) Win32에서는 더 이상 단거리(near)와 장거리(far) 포인터의 구분이 필요 없다. 이것

    은 IBM의 특별한 세그먼트 메모리에서 유래한 것이다. 이 함수는 그러므로, DOS와

    Win16에서만 사용할 수 있다.

FP_SEG(dos.h)

    Far Pointer Segment; 원거리 포인터의 세그먼트 베이스(segment

    base) 주소를 얻는다.

geninterrupt(dos.h)

    Generate Interrupt; 소프트웨어 인터럽트를 발생한다.

getcbrk(dos.h)

    Get Control Break; Control Break 설정을 얻는다.

getdfree(dos.h)

    Get Disk Free; 디스크의 사용 가능 공간을 구한다.

getdta(dos.h)

    Get Disk Transfer Address; DTA의 주소를 얻는다.

getfat(dos.h)

    Get File Allocation Table; 지정된 드라이브의 디스크 할당 테이블의

    정보를 얻는다.

getfatd(dos.h)

    Get File Allocaton Table Drive; 디스크 할당 테이블의 정보를 얻는

    다.

getpsp(dos.h)

    Get Program Segment Prefix; PSP를 얻는다.

getvect(dos.h)

    Get Vector; 인터럽트 벡터를 얻거나 설정한다.

getverify(dos.h)

    Get Veri鱁y; 운영체제의 verify bit를 얻는다.

inp(conio.h)

    Inport; 하드웨어 포트로부터 한 바이트를 읽는다.

inpw(conio.h)

    Inport Word; 하드웨어 포트로부터 한 워드를 읽는다.

inport(dos.h)

    Inport; 하드웨어 포트로부터 한 워드를 읽는다.

inportb(dos.h)

    Inport Byte; 하드웨어 포트로부터 한 바이트를 읽는다.

int86(dos.h)

    Interrupt i8086; i8086 인터럽트를 호출한다.

int86x24)(dos.h)

  24) 단어의 약자로 쓰이는 x는 일반적으로 eXtended 의 약자이다.(발음이 x가 강하기

    때문에 e를 사용하지 않고, x를 사용한다. @x는 Alt+eXit를 나타내는 것이 대표적인 예

    이다.) 이것은 원래 기능에 확장이 되었다는 의미이다. int86x는 int86의 확장된 기능을

    제공한다. Windows\System 폴더에 보면 .OCX(Object Component eXtended Library),

    .VBX(Visual Basic eXtended Library), .VXD(Virtual eXtended Driver)등의 파일들이 있는

    데 여기서 X는 모두 eXtended의 약자이다.

    Interrupt i8086 Extended; i8086 인터럽트를 호출한다.

intdos(dos.h)

    Interrupt DOS; DOS 인터럽트를 호출한다.

intdosx(dos.h)

    Interrupt DOS Extended; DOS 인터럽트를 호출한다.

intr(dos.h)

    Interrupt; i8086 소프트웨어 인터럽트를 호출한다.

MK_FP(dos.h)

    Make Far Pointer; 원거리 포인터로 만든다.

outp(conio.h)

    Out Port; 하드웨어 포트에 한 바이트를 출력한다.

outpw(conio.h)

    Out Port Word; 하드웨어 포트에 한 워드를 출력한다.

outport(dos.h)

    Out Port하드웨어 포트에 한 워드를 출력한다.

outportb(dos.h)

    Out Port Byte; 하드웨어 포트에 한 바이트를 출력한다.

parsfnm(dos.h)

    Parse Filename; 파일 이름을 분석(parse)한다.

peek(dos.h)

    Peek; 메모리의 한 워드를 읽는다.

peekb(dos.h)

    Peek Byte; 메모리의 한 바이트를 읽는다.

poke(dos.h)

    Poke; 메모리에 한 워드를 쓴다.

pokeb(dos.h)

    Poke Byte; 메모리에 한 바이트를 쓴다.

segread(dos.h)

    Segment Read; 세그먼트 레지스터를 읽는다.

setcbrk(dos.h)

    Set Control Break; 컨트롤 브레이크를 설정한다.

_setcursortype(conio.h)

    커서의 모양을 설정한다.

setdta(dos.h)

    Set Disk Transfer Area; DTA를 설정한다.

setvect(dos.h)

    Set Vector; 인터럽트 벡터를 설정한다.

setverify(dos.h)

    운영체제의 verify bit 플래그를 설정한다.

sleep(dos.h)

    실행을 잠시 중단(suspend)한다.

  국제화 지원 함수(International API
Routines)

cprintf(stdio.h)

    Console Print Formatted string; 포맷된 문자열을 출력한다.

cscanf(stdio.h)

    Console Scan from Formatted string; 포맷된 문자열의 형식대로 입

    력 받는다.

fprintf(stdio.h)

    File Printf; 파일에 포맷된 문자열을 출력한다.

fscanf(stdio.h)

    File Scanf; 파일에서 포맷된 문자열의 형식대로 읽는다.

isalnum(ctype.h)

    Is Alphanumeric; 아스키 문자가 알파벳 혹은 숫자인지 검사한다.

isalpha(ctype.h)

    Is Alphabet; 아스키 문자가 알파벳인지 검사한다.

iscntrl(ctype.h)

    Is Control Character; 아스키 문자가 컨트롤 문자인지 검사한다.

isdigit(ctype.h)

    Is Digit Character; 아스키 문자가 숫자 문자인지를 검사한다.

isgraph(ctype.h)

    Is Graphic Character; 아스키 문자가 그래픽 문자 인지를 검사한다.

islower(ctype.h)

    Is Lower Character; 아스키 문자가 소문자인지를 검사한다.

isprint(ctype.h)

    Is Printable Character; 아스키 문자가 프린터 가능한 문자인지를 검

    사한다.

ispunct(ctype.h)

    Is Punctuator Character; 아스키 문자가 구두점 문자인지를 검사한

    다.

isspace	ctype.h

    Is White Space Character; 아스키 문자가 흰 공백 문자인지를 검사

    한다.

isupper	ctype.h

    Is Upper Character; 아스키 문자가 대문자인지를 검사한다.

isxdigit(ctype.h)

    Is Hexdecimal Digit; 아스키 문자가 16진수를 이루는 문자인지를 검

    사한다.

localeconv(locale.h)

    Locale Conversion; 숫자 포맷의 지역 정보를 묻는다.

printf(stdio.h)

    Print Formatted String; 포맷된 문자열을 출력한다.

scanf(stdio.h)

    Scan from Formatted string; 포맷된 문자열의 형식대로 입력 받는

    다.

setlocale(locale.h)

    Set Locale; 지역 정보를 묻거나 선택한다.

sprintf(stdio.h)

    String Print Formatted string; 포맷된 문자열을 스트링에 출력한다.

sscanf(stdio.h)

    String Scan from Formatted string; 포맷된 문자열의 형식대로 스

    트링에서 입력받는다.

strcoll(string.h)

    String Compare using Locale Information; 지역 정보를 이용하여,

    스트링을 비교한다.

strftime(time.h)

    String Format Time; 출력을 위해서 시간 정보를 형식화한다.

strlwr(string.h)

    String Lower; 문자열의 대문자를 소문자로 바꾼다.

strupr(string.h)

    String Upper; 문자열의 소문자를 대문자로 바꾼다.

strx25)frm(string.h)

  25) 일반적으로 x-는 통신 용어에서 trans-의 약자로 사용된다. transmit은 xmit등으로

    나타낸다.

    String Transform; 스트링을 변환시킨다.

tolower(ctype.h)

    To Lower; 문자를 소문자로 변환시킨다.

toupper(ctype.h)

    To Upper; 문자를 대문자로 변환시킨다.

vfprintf(stdio.h)

    Variable argument File Printf; 포맷된 문자열을 스트림에 출

    력한다. 단, 아규먼트(argument) 리스트를 받지 않고, 리스트 아규먼

    트(list of argument)를 받는다.

vfscanf(stdio.h)

    Variable argument File Scanf; 포맷된 문자열의 형식대로 스트림에

    서 입력 받는다.

vprintf(stdio.h)

    포맷된 문자열을 출력한다.

vscanf(stdio.h)

    포맷된 문자열의 형식대로 입력 받는다.

vsprintf(stdio.h)

    Variable argument String Printf; 포맷된 문자열을 스트링에 출력한

    다.

vsscanf(io.h)

    Variable argument String Scanf; 포맷된 문자열의 형식대로 문자열

    에서 입력 받는다.

  조작 함수(Manipulation Routines)

mblen(stdlib.h)

    Multibyte Length; 멀티 바이트 문자의 길이를 구한다.

mbstowcs(stdlib.h)

    Multibyte String To Wide Character String; 멀티 바이트 문자열을

    wchar_t형의 배열로 바꾼다.

mbtowc(stdlib.h)

    Multibyte To Wide Character; 멀티 바이트 문자를 wchar_t형으로

    바꾼다.

memccpy(mem.h, string.h)

    Memory Count Copy; 메모리의 내용을 특정 바이트 복사한다.

memchr(mem.h, string.h)

    Memory Character; 메모리의 내용을 특정 문자로 채운다.

memcmp(mem.h, string.h)

    Memory Compare; 메모리의 내용을 비교한다.

memcpy(mem.h, string.h)

    Memory Copy; 메모리의 내용을 복사한다.

memicmp(mem.h, string.h)

    Memory Ignoring case Compare; 대소문자를 무시하고 메모리의 내

    용을 비교한다.

memmove(mem.h, string.h)

    Memory Move; 메모리의 내용을 복사한다.

memset(mem.h, string.h)

    Memory Set; 메모리의 내용을 특정 값으로 초기화한다.

movedata(mem.h, string.h)

    Move Data; 특정 바이트를 복사한다.

movmem(mem.h, string.h)

    Move Memory; 메모리의 내용을 복사한다.

setmem(mem.h)

    Set Memory; 메모리의 범위에 특정 값을 할당한다.

stpcpy(string.h)

    String Copy; 스트링을 복사한다.

strcat(string.h)

    String Concatenation; 스트링을 접속한다.

strchr(string.h)

    String Character; 스트링을 특정 문자로 채운다.

strcmpi(string.h)

    String Compare Ignoring case; 대소문자를 무시하고 스트링을 비교

    한다.

strcmp(string.h)

    String Compare; 스트링을 비교한다.

strcoll(string.h)

    String Compare Locale Information; 지역 정보를 이용하여 스트링

    을 비교한다.

strcpy(string.h)

    String Copy; 스트링을 복사한다.

strcspn26)(string.h)

  26) pn이 무엇의 약자인지 짐작이 가지 않는다. 도움말을 그대로 옮기면 아래와 같다.

    Description

    Scans a string for the initial segment not containing any subset of a given set of

    characters. The strcspn functions search s2 until any one of the characters contained

    in s1 is found. The number of characters which were read in s2 is the return value.

    The string termination character is not counted. Neither string is altered during the
search.

    Return Value

    strcspn returns the length of the initial segment of string s1 that consists entirely of

    characters not from string s2.

    String Character Scan; 스트링에서 특정 문자들을 검사한다.

strdup(string.h)

    String Duplicate; 스트링을 복사한다.

strerror(string.h)

    String Error; 에러 메시지 스트링의 시작 주소를 리턴한다.

stricmp(string.h)

    String Ignoring case Compare; 대소문자를 무시하고 스트링 비교를

    한다.

strlen(string.h)

    String Length; 스트링의 길이를 구한다.

strlwr(string.h)

    String Lower; 스트링의 대문자를 소문자로 바꾼다.

strncat(string.h)

    String Number Concatenation; 스트링의 일부를 다른 스트링에 접속

    한다.

strncmpi(string.h)

    String Number Compare Ignoring case; 대소문자를 무시하여 스트

    링의 일부를 비교한다.

strncmp(string.h)

    String Number Compare; 스트링의 일부를 비교한다.

strncpy(string.h)

    String Number Copy; 스트링의 일부를 복사한다.

strnicmp(string.h)

    String Number Ignoring case Compare; 대소문자를 무시하여 스트

    링의 일부를 비교한다.

strnset(string.h)

    String Number Set; 스트링의 일부를 특정 문자로 설정한다.

strpbrk(string.h)

    String Break; 특정 문자집합을 스트링에서 찾는다.

strrchr(string.h)

    String Reverse Character; 특정 문자를 거꾸로 찾는다.

strrev(string.h)

    String Reverse; 문자열을 뒤집는다.

strset(string.h)

    String Set; 문자열을 특정 값으로 설정한다.

strspn(string.h)

    String Search; 스트링에서 특정 문자의 집합을 찾는다.

strstr(string.h)

    String Substring; 스트링에서 서브스트링을 검사한다.

strtok(string.h)

    String Token; 스트링에서 토큰(token27))을 얻는다.

  27) 토큰은 컴파일러가 파싱 과정에서 기본적인 입력 단위로 소스를 분리하는 단위이

    다. void main()에서 토큰은 모두 4개이다. 사용자가 필요한 토큰을 분리하기 위해서

    이 함수를 사용한다.

strupr(string.h)

    String Upper; 스트링에서 소문자를 대문자로 바꾼다.

strxfrm(string.h)

    String Transform; 스트링을 지역 정보에 맞도록 변환한다.

wcstombs(stdlib.h)

    Wide Character String To Multibyte String; wchar_t형의 스트링을

    멀티바이트 스트링으로 바꾼다.

wctomb(stdlib.h)

    Wide Character To Mulbibyte Character; wchar_t형의 문자를 멀티

    바이트 문자로 바꾼다.

  수학 함수(Math Routines)

abs(complex.h, stdlib.h)

    Absolute; 절대값(absolute value)을 구한다.

acos(complex.h, math.h)

    Arc Cosine; 아크 코사인을 계산한다.

acosl(math.h)

    Arc Cosine Long; 아크 코사인을 계산한다.

arg(complex.h)

    Angle Radian; 복소 평면상의 각을 라디안으로 구한다.

asin(complex.h, math.h)

    Arc Sine; 아크 사인을 계산한다.

asinl(math.h)

    Arc Sine Long; 아크 사인을 계산한다.

atan(complex.h, math.h)

    Arc Tangent; 아크 탄젠트를 계산한다.

atan2(complex.h, math.h)

    Arc Tangent; 아크 탄젠트를 계산한다.

atan2l(math.h)

    Arc Tangent Long; 아크 탄젠트를 계산한다.

atanl(math.h)

    Arc Tangent Long; 아크 탄젠트를 계산한다.

atof(stdlib.h, math.h)

    ASCII string to Float; 문자열을 부동소수형으로 바꾼다.

atoi(stdlib.h)

    ASCII string to Integer; 문자열을 정수형으로 바꾼다.

atol(stdlib.h)

    ASCII to Long; 문자열을 긴 정수형으로 바꾼다.

_atold(math.h)

    ASCII to Long Double; 문자열을 긴 실수형을 바꾼다.

c28)abs(math.h)

  28) 복소수(complex number) 함수는 모두 c로 시작한다.

    Complex Absolute; 복소수의 절대값을 구한다.

cabsl(math.h)

    Complex Absolute Long; 복소수의 절대값을 구한다.

ceil(math.h)

    Ceiling29); Ceiling을 구한다.

  29) Ceiling은 그 수보다 작지 않은 가장 작은 정수를 구한다. 기호로 ㆃ ㆄ를 사용한다.

    ㆃ3.01ㆄ=ㆃ3.1ㆄ=ㆃ3.9ㆄ=4이다. Floor는 그 수보다 크지 않은 가장 큰 정수를 구한다. ㆓3.01

    ㆔=㆓3.1㆔=㆓3.9㆔=3이다.

ceill(math.h)

    Ceiling Long; Ceiling을 구한다.

_clear87(float.h)

    Clear i8087; 부동 소수 상태를 지운다.

conj(complex.h)

    Conjugate; 공액(conjugate) 복소수를 구한다.

_control87(float.h)

    Control i8087; 부동 소수 제어 단어(word)를 조작한다.

cos(complex.h, math.h)

    Cosine; 코사인을 구한다.

cosh(complex.h, math.h)

    Cosine Hyperbolic; Cosine hyperbolic을 구한다.

coshl(math.h)

    Cosine Hyperbolic Long; Cosine hyperbolic을 구한다.

cosl(math.h)

    Cosine Long; 코사인을 구한다.

div(math.h)

    Divide; 정수 나눗셈을 한다.

ecvt(stdlib.h)

    Exponential Convert; 부동 소수형을 문자열로 바꾼다.

exp(complex.h, math.h)

    Exponential; 지수를 계산한다.

expl(math.h)

    Exponential Long; 지수를 계산한다.

fabs(math.h)

    Floating point number Absolute; 절대값을 구한다.

fabsl(math.h)

    Floating point number Absolute Long; 절대값을 구한다.

fcvt(stdlib.h)

    Floating Convert; 부동 소수형을 문자열로 바꾼다.

floor(math.h)

    Floor; Floor연산을 취한다.(ceil을 참고하라.)

floorl(math.h)

    Floor Long; Floor연산을 취한다.

fmod(math.h)

    Floating Modulo; 나머지 연산을 취한다.

fmodl(math.h)

    Floating Modulo Long; 나머지 연산을 취한다.

_fpreset(float.h)

    Floating Point Reset; 부동 소수 패키지를 다시 초기화한다.

frexp(math.h)

    Floating Exponent; 숫자를 지수(exponent)와 가수(mantissa)로 분리

    한다.

frexpl(math.h)

    Floating Exponent Long; 숫자를 지수(exponent)와 가수(mantissa)

    로 분리한다.

gcvt(stdlib.h)

    General Convert; 부동 소수형을 문자열로 바꾼다.

hypot(math.h)

    Hypotenuse; 직각 삼각형(right triangle)의 사변(hypotenuse)을 구한

    다.

hypotl(math.h)

    Hypotenuse Long; 직각 삼각형(right triangle)의 사변(hypotenuse)을

    구한다.

imag(complex.h)

    Imaginary; 복소수의 허수부를 구한다.

itoa(stdlib.h)

    Integer to ASCII string; 정수를 문자열로 바꾼다.

labs(stdlib.h)

    Long Absolute; 절대값을 구한다.

ldexp(math.h)

    Exponential; x×2y를 계산한다.

ldexpl(math.h)

    Exponential Long;  x×2y를 계산한다.

ldiv(math.h)

    Long Divide; 정수 나눗셈을 한다.

log(complex.h, math.h)

    Logarithm; 로그를 계산한다.

logl(math.h)

    Logarithm Long; 로그를 계산한다.

log10(complex.h, math.h)

    Log base 10; 상용 로그를 계산한다.

log10l(math.h)

    Log base 10 Long; 상용 로그를 계산한다.

_lrotl(stdlib.h)

    Long Rotate Left; 숫자를 왼쪽으로 rotate한다.

_lrotr(stdlib.h)

    Long Rotate Right; 숫자를 오른쪽으로 rotate한다.

ltoa(stdlib.h)

    Long to ASCII string; 긴 정수형을 문자열로 바꾼다.

_matherr(math.h)

    Mathematic Error; 사용자가 변경할 수 있는 수학 에러 핸들러이다.

_matherrl(math.h)

    Mathematic Error Long; 사용자가 변경할 수 있는 수학 에러 핸들

    러이다.

modf(math.h)

    Modulo Floating point number; 정수부와 실수부로 나눈다.

modfl(math.h)

    Modulo Floating point number Long; 정수부와 실수부로 나눈다.

norm(complex.h)

    Normalize; 복소수를 정규화한다.

polar(complex.h)

    Polar; 극 좌표계의 값을 복소수로 바꾼다.

poly(math.h)

    Polynomial; 아규먼트로부터 다항식을 생성한다.

polyl(math.h)

    Polynomial Long; 아규먼트로부터 다항식을 생성한다.

pow(complex.h, math.h)

    Power; 멱승을 구한다.

pow10(math.h)

    Power base 10; 10의 멱승을 구한다.

pow10l(math.h)

    Power base 10 Long; 10의 멱승을 구한다.

powl(math.h)

    Power Long; 멱승을 구한다.

rand(stdlib.h)

    Random number; 난수를 구한다.

random(stdlib.h)

    Random number; 난수를 구한다.

randomize(stdlib.h)

    난수를 초기화한다.

real(complex.h)

    Real number; 복소수의 실수부를 구한다.

_rotl(stdlib.h)

    Rotate Left; 왼쪽으로 rotate한다.

_rotr(stdlib.h)

    Rotate Right; 오른쪽으로 rotate한다.

sin(complex.h, math.h)

    Sine; 사인을 계산한다.

sinh(complex.h, math.h)

    Sine Hyberbolic; 사인 hyperbolic을 계산한다.

sinhl(math.h)

    Sine Hyperbolic Long; 사인 hyperbolic을 계산한다.

sinl(complex.h, math.h)

    Sine Long; 사인을 계산한다.

sqrt(complex.h, math.h)

    Square Root; 루트를 계산한다.

sqrtl(math.h)

    Square Root Long; 루트를 계산한다.

srand(stdlib.h)

    Seed Random; 난수를 초기화한다.

_status87(float.h)

    부동 소수 상태를 얻는다.

strtod(stdlib.h)

    String To Double; 스트링을 실수로 바꾼다.

strtol(stdlib.h)

    String To Long; 스트링을 정수로 바꾼다.

_strtold(stdlib.h)

    String To Long Double; 스트링을 긴 실수로 바꾼다.

strtoul(stdlib.h)

    String To Unsigned Long; 스트링을 부호 없는 정수로 바꾼다.

tan(complex.h, math.h)

    Tangent; 탄젠트를 계산한다.

tanh(complex.h, math.h)

    Tangent Hyperbolic; 탄젠트 hyperbolic을 계산한다.

tanhl(complex.h, math.h)

    Tangent Hyperbolic Long; 탄젠트 hyperbolic을 계산한다.

tanl(math.h)

    Tangent Long; 탄젠트를 계산한다.

ultoa(stdlib.h)

    Unsigned Long To ASCII string; 부호없는 정수를 문자열로 바꾼

    다.

  메모리 함수(Memory Routines)

alloca(malloc.h)

    Allocation; 임시 스택 공간을 할당한다.

_bios_memsize(bios.h)

    BIOS Memory Size; 메모리의 크기를 돌려준다.

calloc(alloc.h, stdlib.h)

    C Allocation; C의 메인 메모리를 할당한다.

farcalloc(alloc.h)

    원거리 힙에서 메모리를 할당한다.

farfree(alloc.h)

    할당된 원거리 힙 메모리를 해제한다.

farmalloc(alloc.h)

    원거리 힙에서 메모리를 할당한다.

free(alloc.h, stdlib.h)

    할당된 블락을 해제한다.

heapcheck(alloc.h)

    힙을 검증(verify)하고 검사(check)한다.

heapcheckfree(alloc.h)

    힙의 사용 가능한 블락을 검사한다.

heapchecknode(alloc.h)

    힙의 한 개의 노드를 검증하고 검사한다.

heapwalk(alloc.h)

    힙을 노드 단위로 따라가면서(walk) 검사한다.

malloc(alloc.h, stdlib.h)

    Memory Allocation; 힙 메모리를 할당한다.

realloc(alloc.h, stdlib.h)

    메인 메모리를 다시 할당한다.

set_new_handler(new.h)

    new의 할당이 실패했을 경우 불리는 핸들러를 설치한다.

stackavail(malloc.h)

    사용 가능한 스택 메모리의 양을 구한다.

  기타 함수(Miscellaneous Routines)

localeconv(locale.h)

    Locale Conversion; 숫자 포맷을 위해 지역 정보를 묻는다.

longjmp(setjmp.h)

    Long Jump; Local이 아닌 곳으로 점프한다.

setjmp(setjmp.h)

    Set Jump; Local이 아닌 곳으로의 goto를 설정한다.

setlocale(locale.h)

    지역정보를 묻거나 선택한다.

  안 쓰이는 함수(Obsolete Functions)

  아래의 함수는 Win95와의 호환성을 위해, 이름이 바뀌었다.

_chmod→_rtl_chmod(io.h)

    Run Time Library Change Mode; 파일 속성을 설정한거나 얻는다.

_close→_rtl_close(io.h)

    파일을 닫는다.

_creat→_rtl_creat(io.h)

    파일을 새로 생성하거나 존재하는 파일에 덮쳐 쓴다.

_heapwalk→_rtl_heapwalk(malloc.h)

    힙을 노드(node)단위로 살핀다.

_open→_rtl_open(io.h)

    파일을 연다.

_read→_rtl_read(io.h)

    파일에서 읽는다.

_write→_rtl_write(io.h)

    파일에 쓴다.

  프로세스 제어 함수(Process Control
Routines)

abort(process.h)

    프로그램을 비정상적으로 종료시킨다.

_beginthread(process.h)

    새로운 쓰레드를 실행한다.

_beginthreadNT(process.h)

    Windows NT에서 새로운 쓰레드를 실행한다.

_c_exit(process.h)

    Clean-up Exit; 프로그램을 종료하지 않고 _exit의 일부를 실행한다.

_cexit(process.h)

    프로그램을 종료하지 않고 _exit의 일부를 실행한다.

cwait(process.h)

    Child Wait; 자식 프로세스가 끝나기를 기다린다.

_endthread(process.h)

    쓰레드를 종료한다.

execle30)(process.h)

  30) 접미어(suffix) l, v, p, e는 계열 이름(family name)이다. l은 Length를 의미하며, 파라

    미터의 수를 미리 알고 있을 때 사용한다. v는 Variable Argument일 때 사용한다. p는

    Path에 지정된 곳에서 파일을 찾는 것을 의미하며, e는 env 환경변수가 자식 프로세

    스에 전달된다는 의미이다.

    Execute, Length Environment; 다른 프로그램을 로드하고 실행한다.

execl(process.h)

    다른 프로그램을 로드하고 실행한다.

execlpe(process.h)

    다른 프로그램을 로드하고 실행한다.

execlp(process.h)

    다른 프로그램을 로드하고 실행한다.

execve(process.h)

    다른 프로그램을 로드하고 실행한다.

execv(process.h)

    다른 프로그램을 로드하고 실행한다.

execvpe(process.h)

    다른 프로그램을 로드하고 실행한다.

execvp(process.h)

    다른 프로그램을 로드하고 실행한다.

_exit(process.h)

    프로그램을 종료한다.

exit(process.h)

    프로그램을 종료한다.

_expand(process.h)

    힙 블락의 크기를 제자리에서(in place) 늘리거나 줄인다.

getpid(process.h)

    Get Process Identifier; 프로그램의 프로세스 ID를 얻는다.

_pclose(stdio.h)

    Piped Close; 파이프된 명령이 완료될 때까지 기다린다.

_popen(stdio.h)

    Piped Open; 명령 프로세스 파이프를 생성한다.

raise(signal.h)

    실행중인 프로그램에 소프트웨어 신호를 전송한다.

signal(signal.h)

    신호 처리 동작(signal-handling action)을 지정한다.

spawnle(process.h)

    자식 프로세스로 알려진 다른 파일을 실행한다.

spawnlpe(process.h)

    자식 프로세스로 알려진 다른 파일을 실행한다.

spawnlp(process.h)

    자식 프로세스로 알려진 다른 파일을 실행한다.

spawnl(process.h)

    자식 프로세스로 알려진 다른 파일을 실행한다.

spawnve(process.h)

    자식 프로세스로 알려진 다른 파일을 실행한다.

spawnvpe(process.h)

    자식 프로세스로 알려진 다른 파일을 실행한다.

spawnvp(process.h)

    자식 프로세스로 알려진 다른 파일을 실행한다.

spawnv(process.h)

    자식 프로세스로 알려진 다른 파일을 실행한다.

wait(process.h)

    한 개 이상의 자식 프로세스가 끝나기를 기다린다.

  Time and Date Routines

asctime(time.h)

    ASCII Time; 날짜와 시간을 문자열로 바꾼다.

_bios_timeofday(bios.h)

    BIOS 타이머를 읽거나 설정한다.

ctime(time.h)

    Convert Time; 날짜와 시간을 문자열로 변경한다.

difftime(time.h)

    Difference Time; 시간의 차이를 구한다.

_dos_getdate(dos.h)

    날짜를 구한다.

_dos_gettime(dos.h)

    시간을 구한다.

_dos_setdate(dos.h)

    날짜를 설정한다.

_dos_settime(dos.h)

    시간을 설정한다.

dostounix(dos.h)

    날짜와 시간을 UNIX 형으로 바꾼다.

ftime(sys\timeb.h)

    Fill Time; 현재의 시간을 timeb 구조체에 저장한다.

getdate(dos.h)

    날짜를 구한다.

gettime(dos.h)

    시간을 구한다.

gmtime(time.h)

    Greenwich Mean Time; 날짜와 시간을 그리니치 표준 시간

    (Greenwich mean time; GMT)으로 바꾼다.

localtime(time.h)

    지역 시간으로 바꾼다.

mktime(time.h)

    Make Time; 시간을 달력 정보로 바꾼다.

stime(time.h)

    System Time; 시스템 날짜와 시간을 설정한다.

_strdate(time.h)

    String Date; 현재 날짜를 스트링으로 바꾼다.

strftime(time.h)

    String Format Time; 시간을 출력을 위해 형식화한다.

_strtime(time.h)

    String Time; 현재 시간을 출력을 위해 형식화한다.

time(time.h)

    현재의 시간을 얻는다.

tzset(time.h)

    Time Zone Set; 전역변수 _timezone, _daylight, _tzname을 설정한

    다.

unixtodos(dos.h)

    UNIX to DOS; UNIX의 날짜와 시간을 DOS로 변경한다.

  가변 인자 리스트함수(Variable Argument
List Routines)31)

  31) 이 함수들의 자세한 사용은 "가변 인자"를 참고하라.

va_start(stdarg.h)

    Variable Argument Start; 가변 인자 리스트를 구현한다.

va_arg(stdarg.h)

    Variable Argument Argument; 가변 인자 리스트를 구현한다.

va_end(stdarg.h)

    Variable Argument End; 가변 인자 리스트를 구현한다.

  헤더 파일 요약

  아래에 볼런드 컴파일러의 헤더 파일과 이에 대응하는 마이크로소프트 컴
파일러의 헤더 파일을 비교 제시하였다. 두 컴파일러가 지원하는 함수가 정확
히 같은 것은 아니지만, 대부분은 같다.

Borland C++ 4.5

Visual C++ 4.2

설명

alloc.h

malloc.h

Declares memory-management
functions (allocation, deallocation, and
so on).
assert.h

assert.h

Defines the assert debugging macro.
bcd.h

클래스

Declares the C++ class bcd and the
overloaded operators for bcd and bcd
math functions.

bios.h

도스

Declares various functions used in
calling IBM-PC ROM BIOS routines.
bwcc.h

Defines the Borland Windows Custom
Control interface.

Borland C++ 4.5

Visual C++ 4.2

설명

checks.h

진단 매크로

Defines the class diagnostic macros.

complex.h

complex.h

Declares the C++ complex math
functions.

conio.h

conio.h

Declares various functions used in
calling the operating system console
I/O routines.
constrea.h

Defines the conbuf and constream
classes.

cstring.h

cstring.h
Defines the string classes.

ctype.h

ctype.h

Contains information used by the
character classification and character
conversion macros (such as isalpha
and toascii).

date.h

클래스

Defines the date class.

_defs.h

Defines the calling conventions for
different application types and memory
models.

dir.h

도스

Contains structures, macros, and
functions for working with directories
and path names.

direct.h

direct.h

Defines structures, macros, and
functions for dealing with directories
and path names.

dirent.h

Declares functions and structures for
POSIX directory operations.

dos.h

dos.h

Defines various constants and gives
declarations needed for DOS and
8086-specific calls.
errno.h

errno.h

Defines constant mnemonics for the
error codes.

except.h

eh.h, 클래스

Declares the exception-handling
classes and functions.

excpt.h

excpt.h

Declares C structured exception
support.

fcntl.h

fcntl.h

Defines symbolic constants used in
connection with the library routine
open.

Borland C++ 4.5

Visual C++ 4.2

설명

file.h

클래스

Defines the file class.

float.h

float.h

Contains parameters for floating-point
routines.
fstream.h

fstream.h

Declares the C++ stream classes that
support file input and output.
generic.h

Contains macros for generic class
declarations.

io.h

io.h

Contains structures and declarations
for low-level input/output routines.
iomanip.h

iomanip.h

Declares the C++ streams I/O
manipulators and contains templates
for creating parameterized
manipulators.
iostream.h

iostream.h

Declares the basic C++ streams (I/O)
routines.

limits.h

limits.h

Contains environmental parameters,
information about compile-time
limitations, and ranges of integral
quantities.
locale.h

locale.h

Declares functions that provide
country- and language-specific
information.

malloc.h

malloc.h

Declares memory-management
functions and variables.
math.h

math.h

Declares prototypes for the math
functions and math error handlers.

mem.h

memory.h

Declares the memory-manipulation
functions. (Many of these are also
defined in string.h.)

memory.h

memory.h

Contains memory-manipulation
functions.
new.h

new.h

Access to _new_handler, and
set_new_handler.

_nfile.h

Defines the maximum number of open
files.

_null.h

Defines the value of NULL.

Borland C++ 4.5

Visual C++ 4.2

설명

process.h

process.h

Contains structures and declarations
for the spawn... and exec... functions.

search.h

search.h

Declares functions for searching and
sorting.
setjmp.h

setjmp.h

Declares the functions longjmp and
setjmp and defines a type jmp_buf
that these functions use.

share.h

share.h

Defines parameters used in functions
that make use of file-sharing.
signal.h

signal.h

Defines constants and declarations for
use by the signal and raise functions.
stdarg.h

stdarg.h

Defines macros used for reading the
argument list in functions declared to
accept a variable number of arguments
(such as vprintf, vscanf, and so on).
stddef.h

stddef.h

Defines several common data types
and macros.
stdio.h

stdio.h

Defines types and macros needed for
the standard I/O package defined in
Kernighan and Ritchie and extended
under UNIX System V. Defines the
standard I/O predefined streams stdin,
stdout, stdprn, and stderr and declares
stream-level I/O routines.
stdiostr.h

stdiostr.h

Declares the C++ (version 2.0) stream
classes for use with stdio FILE
structures. You should use iostream.h
for new code.
stdlib.h

stdlib.h

Declares several commonly used
routines such as conversion routines
and search/sort routines.
string.h

string.h

Declares several string-manipulation
and memory-manipulation routines.

strstrea.h

strstrea.h

Declares the C++ stream classes for
use with byte arrays in memory.

sys\locking.h

sys\locking.h

Contains definitions for mode
parameter of locking function.

Borland C++ 4.5

Visual C++ 4.2

설명

sys\stat.h

\sys\stat.h

Defines symbolic constants used for
opening and creating files.
sys\timeb.h

\sys\timeb.h

Declares the function ftime and the
structure timeb that ftime returns.

sys\types.h

\sys\types.h

Declares the type time_t used with
time functions.

thread.h

클래스

Defines the thread classes.

time.h

time.h

Defines a structure filled in by the
time-conversion routines asctime,
localtime, and gmtime, and a type
used by the routines ctime, difftime,
gmtime, localtime, and stime. It also
provides prototypes for these routines.

typeinfo.h

typeinfo.h

Declares the run-time type information
classes.

utime.h

sys/utime.h

Declares the utime function and the
utimbuf struct that it returns.

values.h

UNIX 호환

Defines important constants, including
machine dependencies; provided for
UNIX System V compatibility.

varargs.h

varargs.h

Definitions for accessing parameters in
functions that accept a variable
number of arguments. Provided for
UNIX compatibility; you should use
stdarg.h for new code.

  ANSI에서 정한 표준 함수는 양쪽의 컴파일러에서 모두 지원하고 있다. 하
지만, 표준이 없는 Windows의 함수나 클래스들, 최적화를 위한 함수들, 진단
매크로(diagnostic macro)들, 매킨토시(Macintosh)나 유닉스(UNIX) 호환 함수
들은 양쪽 컴파일러에서 모두 지원하고 있지만, 함수의 이름이나 헤더파일의
이름이 통일된 것은 아니다. 또한 도스(DOS) 전용 함수로 쓰이던 것들은,
Visual C++에서 전혀 지원하지 않는다. 이것은 Visual C++가 도스용 응용 프
로그램을 개발하는 환경을 지원하고 있지 않기 때문이다.

  연습문제

1.


26. 클래스 계층도를 그리는 법

26. 클래스 계층도(class
hierarchy)를 그리는 법

  OOL(object oriented language)에서는 알고리즘뿐만 아니라, 문제 설계 그
자체가 중요한 개념으로 대두되었다. 문제의 자세한 풀이를 시작하기 전에, 문
제를 기술하는 것이 효율적으로 문제를 풀이하는데 많은 도움을 줄 수 있다.

  이 장에서는 클래스 계층도를 그리기 위해 제안된 표준 방법들1)을 살펴

            1) 이 표준은 대부분이 UML(unified modeling language) 표기법에 따른 것이다. 참고
          서적, Martin Fowler with Kendall Scott, (Booch, Jacobson, Rumbaugh,) "UML
          DISTILLED", ADDISON-WESLEY, 1997을 보라.

볼 것이다. 이 책에서 제공하는 많은 예제들은 이 표준을 가능한 따를 것이지
만, 의미가 명확하다면, 표준을 따르지 않을 수도 있을 것이다.

  클래스 계층도는 클래스간의 상속 관계 및 포함 관계를 나타내기 위하여,
특정한 심벌을 사용해서 그린 그림이다. 많은 사람들은 클래스를 나타내기 위
해, 네모(rectangle)를 사용한다. 또한 클래스의 멤버 변수와 멤버 함수를 각각
의 네모에 차례대로 표시하고, 클래스의 이름은 誌은(bold) 글씨체를 사용한
다.

            그림 26-1 클래스는 네모를 사용하여 그린다. 클래스 이름과 멤버 변수, 멤버 함수
          를 각각의 네모에 나타낸다.

  <그림 26-1>은 클래스를 나타낸다. 클래스가 네모로 표시되었을 때, 첫 번
째 칸에는 클래스의 이름을 굵은 글씨체로 쓴다. 두 번째 네모에는 멤버 변수
(member variable)를 명시하며, 세 번째 칸에는 멤버 함수를 명시한다. <그림
26-2>에서는 key와 list[]를 멤버 변수로 가지고, CopyList()를 멤버 함수로
가지는 CSort라는 클래스를 표시한다.

  상속(inheritance)은 속이 빈 화살표(hollowed arrow)를 사용하여 나타낸다.

            그림 26-2 상속은 속이 빈 화살표를 사용한다.

  <그림 26-2>는 CSort를 상속 받는 CMerge 클래스를 나타낸다. 상속은 클
래스 계층도에서 IS-A 혹은 is kind of로 표현된다. 위의 <그림 26-2>는
CMerge is a CSort.라는 문장을 표현한다. 그러므로 화살표가 위로 가는 것
이 자연스럽다. 일반화2)를 나타내기 위해 속이 빈 화살표를 사용하는 것은

            2) 상속은 일반화의 반대과정이다. 상속을 받을 수록 일반적인 사항들을 공유하지
          만, 상세한 부분들은 점점 늘어난다.

UML 권고 사항이다.

            그림 26-3 HAVE-A의 표현은 속이 찬 화살표(solid arrow) -、를 사용한다.

  클래스의 멤버 변수로 다른 클래스의 레퍼런스 혹은 객체 배열을 포함한 경
우는 속인 찬 화살표(solid arrow)를 사용한다. <그림 26-3>은 CMerge가
CData를 멤버 변수로 가짐을 의미한다. 이것은 영어 문장 CMerge have a
CData 혹은 CData is part of CMerge를 의미하므로, HAVE-A, IS PART
OF 관계라고도 한다. HAVE-A는 상속을 나타내는 것이 아니라, 포함 관계를
나타낸다. 화살표의 방향에 주의하라. CData에 CMerge의 인스턴스 멤버가 존
재하는 것이 아니라, CMerge안에 CData의 인스턴스가 존재한다. 그러므로 <
그림 26-3>의 소스는 아래와 같은 형식이 될 것이다.

class CSort {

    int key;

    int list[];

  public:

    void CopyList();

};

class CData {

    int data;

  public:

    int Get();

    void Set();

};

class CMerge : public CSort {

    int size;

    CData d;

  public:

    void MergeSort();

};

            그림 26-4 템플릿의 클래스 표현

  일반화된 클래스(generalized class)의 파라미터를 나타내기 위해서는 어떻
게 할 것인가? 클래스의 오른쪽 상단에 점선 박스로 템플릿의 파라미터 타입
을 명시하여 나타낸다. <그림 26-4>는 형 T를 파라미터로 받는 일반화된
CStack클래스를 나타낸다.

            그림 26-5 have-a 관계의 여러 표현

  HAVE-A 관계는 데이터 멤버의 수가 설계에 고려되어야 한다. 그러므로
이를 그림에 명시할 수 있어야 할 것이다. <그림 26-5>를 참조하라. 이것은
표준 UML 표현3)이 아니지만, 전산이론을 공부한 사람에게는 UML 표현보다

            3) 이것은 Booch가 사용한 표현이지만, 표준 표현이기도 하다. +의 UML 표현은 1..*
          이다. ?의 UML 표현은 0..1이다. *의 UML 표현은 *로 같다.

더 친숙할 것이다. 형식 언어(formal language)와 오토마타 이론(automata
theory)에서 ?는 0 혹은 1을 의미한다. +는 한 번 이상의 반복을 의미하며, *
는 0번 이상의 반복을 의미한다.

  예를 들면, a+는 {a, aa, aaa, ...}이다. ab+는 {ab, abb, abbb, ...}이다. a*는
{NIL, a, aa, aaa, ...}이다. a?는 {NIL, a}이다.

  그렇다면, 위의 관계가 무엇을 의미하는지 알 수 있을 것이다. 예를 들면,
헤드를 가지는 링크드 리스트는, 헤드를 나타내는 클래스 CHeader와 노드 데
이터 CNode사이에는 CHeader →* CNode의 관계가 존재할 것이다.

Visio 5

  이 책의 모든 그림은 대부분 Visio™ 4를 이용하여 그린 것이다. 후에 Visio
5를 사용하게 되었는데, Visio 5에서는 UML을 스텐실(stencil)로 제공하고 있
으므로, 클래스를 그리기가 쉬워졌다. 하지만, 이 책에서는 편집상의 이유로 -
이미 많은 그림이 Visio 4로 그려졌다 - 표준 UML을 사용하지 않는 클래스
그림도 있다. 후의 개정판에서 이 부분은 수정될 것이다.

             Visio 5에서는 UML 기호를 스텐실로 제공한다.

  연습문제

1. 자동차를 CCar로 나타내기로 하자. 바퀴(CWheel)와 차체(CBody)를 가지는
스포츠카 CSportsCar 클래스를 구현하려 한다. 클래스 계층도를 그리라.

2.


29. 선언 변경자(declaration modifier)

29. 변경자(modifier)

  독자들은 .EXE 파일을 역 어셈블(reverse assemble)■해 본 적이 있는가?

             만들어진 실행 파일을 입력으로 받아 어셈블러 소스를 출력으로 보내는
          작업을 역어셈블이라 한다.

  그때 C 언어에서 사용한 명칭(identifier)의 앞에 붙은 언더스코어
(underscore: _)⼆를 이상하게 생각해 본 적이 없는가? 경험이 없다면 아래의

             언더스코어(underscore)와 언더바(underbar)를 구분하라. _a는 '언더스코어
          에이'라 읽고, a는 '에이 언더바'라 읽는다.

예를 보자(아래의 예는 역 어셈블이 아니라, C++ 소스의 컴파일 결과를 어셈
블러 소스로 만드는 작업이다. 모든 컴파일러가 이것을 지원한다).

int counter;

int Func(int temp) {

    return temp*2;

}//Func()

void main() {

    counter=1;

    Func(counter);

}

  위의 프로그램을 볼런드 C++ 3.1에서 컴파일하여, 어셈블러 소스를 생성해
보자(어셈블된 소스의 길이를 짧게 하기 위해 표준함수를 사용하지 않았다).

             Options->Compiler->Code Generation 대화 상자: 컴파일하면 어셈블러 소
          스를 생성하도록 설정할 수 있다.

  어셈블된 소스는 아래와 같다. 필자는 독자들이 어셈블러 소스를 모두 분석
하기를 원치 않는다(독자들도 그러할 것이다). 각각의 소스에서 강조된
(highlighted) 부분을 서로 비교해 가며 보기 바란다.

① 소스 파일 이름이 TEST.C 일때

[TEST.ASM의 소스]

    .386p

    ifndef	??version

?debug	macro

    endm

publicdll macro	name

    public	name

    endm

    endif

    ?debug	V 300h

    ?debug	S "TEST.C"

    ?debug	C E9C7027C2606544553542E43

TEST_TEXT	segment byte public use16 'CODE'

TEST_TEXT	ends

DGROUP	group	_DATA,_BSS

    assume	cs:TEST_TEXT,ds:DGROUP

_DATA	segment word public use16 'DATA'

d@	label	byte

d@w	label	word

_DATA	ends

_BSS	segment word public use16 'BSS'

b@	label	byte

b@w	label	word

_BSS	ends

TEST_TEXT	segment byte public use16 'CODE'

    ?debug	C E80106544553542E43C7027C26

   ;

   ;	int Func(int temp) {

   ;

    ?debug	L 3

    assume	cs:TEST_TEXT

_Func	proc	far

    ?debug	B

    push	bp

    mov	bp,sp

    ?debug	C E60474656D70040A060000

    ?debug	B

   ;

   ;	    return temp*2;

   ;

    ?debug	L 4

    mov	ax,word ptr [bp+6]

    shl	ax,1

    jmp	short @1@58

@1@58:

   ;

   ;	}//Func()

   ;

    ?debug	L 5

    pop	bp

    ret

    ?debug	C E60474656D70040A060000

    ?debug	E

    ?debug	E

_Func	endp

   ;

   ;	void main() {

   ;

    ?debug	L 7

    assume	cs:TEST_TEXT

_main	proc	far

    ?debug	B

    push	bp

    mov	bp,sp

    ?debug	B

   ;

   ;	    counter=1;

   ;

    ?debug	L 8

    mov	word ptr DGROUP:_counter,1

   ;

   ;	    Func(counter);

   ;

    ?debug	L 9

    push	word ptr DGROUP:_counter

    push	cs

    call	near ptr _Func

    pop	cx

   ;

   ;	}

   ;

    ?debug	L 10

    pop	bp

    ret

    ?debug	E

    ?debug	E

_main	endp

TEST_TEXT	ends

_BSS	segment word public use16 'BSS'

_counter	label	word

    db	2 dup (?)

    ?debug	C E9

    ?debug	C FA00000000

_BSS	ends

_DATA	segment word public use16 'DATA'

s@	label	byte

_DATA	ends

TEST_TEXT	segment byte public use16 'CODE'

TEST_TEXT	ends

    public	_main

    public	_Func

    public	_counter

_s@	equ	s@

    ?debug	C EA010C

    ?debug	C E31800000023010400

    ?debug	C EC055F6D61696E181800

    ?debug	C E31900000023040400

    ?debug	C EC055F46756E63191800

    ?debug	C EC085F636F756E746572040000

    end

② 소스 파일 이름이 TEST.CPP 일때

[TEST.ASM의 소스]

    ifndef	??version

?debug	macro

    endm

publicdll macro	name

    public	name

    endm

$comm	macro	name,dist,size,count

    comm	dist name:BYTE:count*size

    endm

    else

$comm	macro	name,dist,size,count

    comm	dist name[size]:BYTE:count

    endm

    endif

    ?debug	V 300h

    ?debug	S "test.cpp"

    ?debug	C E940007C2608746573742E637070

_TEXT	segment byte public 'CODE'

_TEXT	ends

DGROUP	group	_DATA,_BSS

    assume	cs:_TEXT,ds:DGROUP

_DATA	segment word public 'DATA'

d@	label	byte

d@w	label	word

_DATA	ends

_BSS	segment word public 'BSS'

b@	label	byte

b@w	label	word

_counter	label	word

    db	2 dup (?)

_BSS	ends

_TEXT	segment byte public 'CODE'

   ;

   ;	int Func(int temp) {

   ;

    assume	cs:_TEXT

@Func$qi	proc	near

    push	bp

    mov	bp,sp

   ;

   ;	    return temp*2;

   ;

    mov	ax,word ptr [bp+4]

    shl	ax,1

    jmp	short @1@58

@1@58:

   ;

   ;	}//Func()

   ;

    pop	bp

    ret

@Func$qi	endp

   ;

   ;	void main() {

   ;

    assume	cs:_TEXT

_main	proc	near

    push	bp

    mov	bp,sp

   ;

   ;	    counter=1;

   ;

    mov	word ptr DGROUP:_counter,1

   ;

   ;	    Func(counter);

   ;

    push	word ptr DGROUP:_counter

    call	near ptr @Func$qi

    pop	cx

   ;

   ;	}

   ;

    pop	bp

    ret

_main	endp

    ?debug	C E9

    ?debug	C FA00000000

_TEXT	ends

_DATA	segment word public 'DATA'

s@	label	byte

_DATA	ends

_TEXT	segment byte public 'CODE'

_TEXT	ends

    public	_main

    public	@Func$qi

    public	_counter

_s@	equ	s@

    end

③ 소스 파일 이름이 TEST.CPP 이며, counter와 Func()에 pascal을 명시했
을 때

[바꾼 소스]

int pascal counter;

int pascal Func(int temp) {

    return temp*2;

}//Func()

void main() {

    counter=1;

    Func(counter);

}

[TEST.ASM의 소스]

    ifndef	??version

?debug	macro

    endm

publicdll macro	name

    public	name

    endm

$comm	macro	name,dist,size,count

    comm	dist name:BYTE:count*size

    endm

    else

$comm	macro	name,dist,size,count

    comm	dist name[size]:BYTE:count

    endm

    endif

    ?debug	V 300h

    ?debug	S "test.cpp"

    ?debug	C E90F017C2608746573742E637070

_TEXT	segment byte public 'CODE'

_TEXT	ends

DGROUP	group	_DATA,_BSS

    assume	cs:_TEXT,ds:DGROUP

_DATA	segment word public 'DATA'

d@	label	byte

d@w	label	word

_DATA	ends

_BSS	segment word public 'BSS'

b@	label	byte

b@w	label	word

COUNTER	label	word

    db	2 dup (?)

_BSS	ends

_TEXT	segment byte public 'CODE'

   ;

   ;	int pascal Func(int temp) {

   ;

    assume	cs:_TEXT

@FUNC$QI	proc	near

    push	bp

    mov	bp,sp

   ;

   ;	    return temp*2;

   ;

    mov	ax,word ptr [bp+4]

    shl	ax,1

    jmp	short @1@58

@1@58:

   ;

   ;	}//Func()

   ;

    pop	bp

    ret	2

@FUNC$QI	endp

   ;

   ;	void main() {

   ;

    assume	cs:_TEXT

_main	proc	near

    push	bp

    mov	bp,sp

   ;

   ;	    counter=1;

   ;

    mov	word ptr DGROUP:COUNTER,1

   ;

   ;	    Func(counter);

   ;

    push	word ptr DGROUP:COUNTER

    call	near ptr @FUNC$QI

   ;

   ;	}

   ;

    pop	bp

    ret

_main	endp

    ?debug	C E9

    ?debug	C FA00000000

_TEXT	ends

_DATA	segment word public 'DATA'

s@	label	byte

_DATA	ends

_TEXT	segment byte public 'CODE'

_TEXT	ends

    public	_main

    public	@FUNC$QI

    public	COUNTER

_s@	equ	s@

    end

  위의 3가지 경우의 소스를 보면 명칭(identifier)의 이름 앞에 어떤 것은 언
더스코어(_)가 붙었고 어떤 것은 그렇지 않다. 또한, 명칭의 이름이 모두 대문
자로 바뀐 경우도 있다. 언더스코어가 붙은 함수는 C 호출 관례(C calling
convention)을 따르는 함수임을 나타내며, 언더스코어가 없고 모두 대문자인
함수는 Pascal 호출 관례(Pascal calling convention)를 따르는 것을 나타
낸다.

             파스칼(Pascal): 그는 수학자이며 과학자였다. 그의 이름을 기리기 위해 교
          육용 컴퓨터 언어에 그의 이름이 채택되었다. 파스칼 언어에서 사용한 파
          라미터 전달 방법을 C++에서 사용하기 위해서는 pascal을 명시해야 한다.

  아주 특이하게 이름이 변경된

@Func$qi

같은 경우도 있다. 여기서 @는 이 함수가 파라미터 전달 방법으로 레지스터
를 사용했음을 나타낸다. $qi는 C++의 이름 장식(name mangling)때문에 붙
여진 문자들이다. 이름 장식은 '함수 이름 오버로딩(function name
overloading)'에서 자세히 다룬다.

  이 장에서는 이처럼 원래의 기능에 추가적인 변경을 가하기 위해 사용하는
C++의 변경자(modifier)에 대해서 알아본다. 또한 cv-한정자(cv-qualifier)
라고 알려진 const와 volatile, mutable과 explicit같은 지정자(specifier)에 대
해서도 알아본다⼆.

             변경자와 지정자등은 문맥에 따라 적절히 사용된다. 예를 들면, 클래스를
          정의하는 입장에서 private:, public: 등은 접근 권한(access rights)을 지정하
          기 때문에 접근 지정자(access specifier)이다. 하지만, 상속에서 상위 클
          래스의 접근 권한을 변경하기 위해서 사용하는 private :, public : 등은 접
          근 변경자(access modifier)라 한다. const, volatile은 변수나 함수의 기능
          에 제한을 가하기 위해서 사용하기 때문에 한정자(qualifier)라 한다. 하지
          만, 이러한 차이점을 알고 있다면, 모두에 대해서 변경자(modifier)란 용어
          를 사용해도 무방할 것이다.

   변경자의 종류

  변수 선언(variable declaration)에는 변수의 형 이름(type name)과 변수
이름(명칭, identifier)이 필요하다. 예를 들면,

int i;

는 정수형 변수 i를 선언한 것이다. 또한, 변수의 기억 부류(storage class)
를 지정(specification)하기 위해 auto, static, extern 및 register를 형 이름 앞에
명시할 수 있다. 예를 들면,

extern int i;

는 자신 이외의 파일에 이미 존재하는 정수형 변수 i를 참조할 것을 선언한
것이다. 변수를 선언하기 위해, 형 이름과 기억 부류 지정자(storage class
specifier)외에 몇 가지 키워드를 명시할 수 있는데, 이것을 일반적으로 변경
자(modifier)라 한다. 이것은 함수의 선언에서도 마찬가지이다. 함수의 리턴
형과 이름 사이에 변경자를 명시할 수 있다. 다음은 대부분의 컴파일러에서
지원하는 변경자들이다. 이 중 어떤 것들은 Windows에서만 사용하는 것들이
고, ISO의 표준이 아니지만, 기능들은 모두 숙지(know throughly)하기 바란다
(실제의 키워드는 선행 언더스코어가 2개씩 붙은 형태이다. 예를 들면 cdecl은
__cdecl일 것이다. 이것은 이러한 키워드가 ISO의 표준이 아닌데서 기인한 것
이다. 하지만, cdecl은 컴파일 시간에 __cdecl로 번역된다).

변경자		사용하는 곳	설명

const		변수		객체의 변경을 금지한다.

volatile		변수		레지스터 할당 및 몇 최적화를 금지한다.

                이 변수를 참조하는 코드는 어떤 최적화도

                하지 않기 때문에 값을 읽어올 때 반드시

                메모리의 현재 값을 읽어 온다.

cdecl		함수		C 파라미터 전달 방법을 명시한다. 링커와

                링크 시간 이름(link-time name)■에 영향

             우리가 선언한 변수나 함수의 명칭(identifier)은 실행 코드를 생성할 때, 컴
          파일러에 의해 적절히 바뀐다. 예를 들면, C++에서 오버로딩(overloading)된
          함수들의 이름을 유일하게 구분하기 위해, 함수의 이름을 바꾸는데 이것을
          이름 장식(name mangling)이라 한다. 또한, 함수의 파라미터 전달 방법에
          따라, 코드의 차이가 존재하므로, 이것을 구분하기 위해, 함수의 이름 앞에
          특수문자를 덧붙여 이를 구분한다. 이렇게 변경된 함수의 이름을 링크 시
          간 이름(link-time name)이라 한다. 이것은 변수의 경우도 마찬가지이다.
          자세한 설명은 이 장의 호출 관례(calling convention)를 참고하라.

                을 미친다. C DECLaration의 약자이다.

cdecl		변수		명칭이 대소문자를 구분하며, 선행 언더스

                코어(leading underscore) _가 붙는다.

interrupt		함수		인터럽트 핸들러를 작성할 때 추가적인 레

                지스터 상태보관(register-housekeeping) 코

                드를 생성한다. '인터럽트'에서 사용한 바

                있다.

pascal		함수		파스칼 파라미터 전달방법을 명시한다. 링

                커와 링커 시간 이름에 영향을 미친다.

pascal		변수		명칭의 대소문자를 무시(case-insensitivity)하

                며, 선행 언더스코어는 붙지 않는다.

near		포인터 형	현재 메모리 모델에 명시된 기정의 포인터

                타입을 오버라이드한다. 세그멘테이션에서

                16비트 포인터를 의미한다.

far,huge		포인터 형	현재 메모리 모델에 명시된 기정의 포인터

                타입을 오버라이드한다. 세그멘테이션에서

                32비트 포인터를 의미한다. huge는 포인터

                의 정규화(pointer normalization)를 수행하며,

                far는 그렇지 않다.

__cs,__ds,

__es,_ss		포인터 형	세그멘테이션에서 세그먼트 포인터 임을 나

                타낸다. 대부분의 프로그램에서 이것을 직

                접 사용할 일은 거의 없다.

export		함수/클래스	수출(export)될 함수/클래스임을 컴파일러에

                게 알려 준다. DLL 등에서 함수를 외부에

                공개하기 위해 사용한다(Windows only).

import		함수/클래스	수입(import)될 함수/클래스임을 컴파일러에

                게 알려 준다. 일반적으로 Export된 함수를

                사용하기 위해 링커에 의해 사용된다

                (Windows only).

__loadds		함수		현재 데이터 세그먼트를 가리키도록 DS를

                설정한다.

__saveregs	함수		함수의 수행 동안 모든 레지스터 값을 보존

                한다.

fastcall		함수		파라미터 전달을 위해 레지스터를 사용

                (register parameter passing)할 것을 지시한

                다. 링커와 링크 시간 이름에 영향을 미친

                다.

stdcall		함수		표준 WIN32 파라미터 전달 방법(WIN32

                parameter passing)을 사용할 것을 지시한

                다(이것은 pascal과 비슷하지만 같지는 않

                다).

__rtti		클래스		클래스의 실행시 형 정보(RunTime Type

                Information)를 지원한다(이 키워드는 RTTI가

                C++의 표준에 채택되고 나서 무의미해 졌

                다).

   한정자(qualifier⼆): const와 volatile

             혹은 지정자(specifier)라고도 한다.

  const는 변수의 선언과 함께 사용되어 변수의 상수(constant) 속성을 지정
한다. volatile은 같은 용도로 변수의 휘발(volatile) 속성을 설정한다.

int i=1;

과

const int i=1;

은 다르다. const 한정자가 명시되면, i는 상수 취급되므로, i에 어떤 변경도
가할 수 없다. 만약

i=i+1;

처럼 i값을 변경하려는 코드가 발견되면, 컴파일러는 컴파일 시간에 에러 메
시지를 출력한다. 이것은 어떤 값이 읽을 목적으로만 사용될 것을 확실히 보
장한다.

  포인터를 상수로 선언할 때는 추가적인 주의가 필요하다.

const char* s="Do not touch me!";

위 선언은 s의 내용(*s)을 상수로 선언한 것이다. 그러므로, *(s+1)='O'; 는 불
법이다. 하지만, s="Hello"; 는 불법이 아니다. 하지만,

char* const s="Do not see me!";

는 s를 상수로 선언한 것이다. 그러므로, *(s+1)='O'; 는 불법이 아니다. 하지
만, s="Hello"; 는 불법이다. 아래의 예를 참고하라.

#include <stdio.h>

int i=1;

const int j=2;//j는 메모리에 할당되었지만, 변경할 수 없는 상수이다.

const char *s="Do not touch me!";//*s is constant

char* const t="Do not see me!";

void main() {

    //*(s+1)='O';//이 문장은 컴파일 시간 에러를 유발한다.

    s="I can touch you!";

    *(t+1)='O';

    //t=s;//이 문장은 컴파일 시간 에러를 유발한다.

    printf("%s\n",s);

    printf("%s\n",t);

}//main()

  실행 결과는

    I can touch you!

    DO not see me!

이다.

  const의 또 다른 사용은 함수의 리턴값이 바뀌지 않는다는 보장을 위한 ①
리턴 값의 상수 선언과 파라미터의 값이 바뀌어서는 안된다는 보장을 위한 ②
파라미터의 상수 선언 및 클래스의 멤버 함수가 어떤 값도 바꾸어서는 안된다
는 보장을 위한 ③멤버 함수의 상수 선언이 있다. 아래의 소스를 보자.

#include <stdio.h>

int i=1;

const int* ip;

int* ip2;

const int* f() {

    return &i;

}

void main() {

    ip=f();

    //ip2=f();//이 문장은 컴파일 시간 에러이다.

    printf("%d\n",*ip);

}//main()

  ①위의 소스에서 f()가 리턴하는 값은 반드시 const int* 형의 변수에 대입
되어야 한다. 이것은 f()가 리턴하는 값이 가리키는 내용(*ip)을 수정하려는 것
을 예방한다.

  ②어떤 함수의 파라미터에 const가 명시되면, 함수 내부에서 그 파라미터를
수정하려는 것은 불법이다. 예를 들면, printf()의 원형(prototype)을 참고해 보
자(볼런드 C++ 4.5의 stdio.h에서 가져온 원형이다. 각자가 사용하는 컴파일러
의 stdio.h를 열어서 printf를 찾아 보라).

int     _RTLENTRY _EXPFUNC printf(const char * __format, ...);

  위의 함수 선언에서 파라미터에만 주목해 보자. 첫 번째 문자열 __format이
const로 선언되어 있다. 이것은 printf()가 내부에서 __format에 어떤 변경도
가하지 않는다는 것을 의미한다. 이렇게 반문해 볼 수도 있다.

'내가 만드는 함수에서 파라미터의 값을 바꾸지 않는 것이 확실한데, 굳이
파라미터에 const를 명시할 필요가 있는가?'

  물론 그렇다. 사실 파라미터의 const 지정은 무의미한 것이다. 하지만, 이것
은 문서화(documentation) 및 안전한 코딩(safe coding)에 분명 도움이 된다.
const를 명시함으로써 printf()의 __format이 내부적으로 읽기 목적으로만 사
용된다는 것을 우리는 알 수 있다. 우리가 작성한 함수에 대해서도 마찬가지
이다. 후에 코드를 읽을 때, 함수의 파라미터의 역할을 이해하는데 분명 도움
이 될 것이다.

  ③const에 대한 마지막 예는 멤버 함수(member function) 자체의 const
선언이다. 이것은 C++의 주제이므로, 클래스를 접해보지 않은 독자는 'C++의
구조체'를 먼저 읽어보기 바란다.

  멤버 함수를 상수로 선언한다는 것은, 멤버 함수가 클래스의 어떤 값도 변
경시키지 않아야 하는 것을 의미한다. C++의 설계자는 이것의 문법 표현을
어떻게 해결했을까? const는 함수의 선언에서 어디에 위치해야 할까?

  const의 위치는 상당히 재미있다. 놀랍게도 const는 함수의 닫는 괄호 뒤에
명시한다. 예를 들면, CTest의 멤버 함수 Print()를 상수 함수로 선언하려면,

void CTest::Print() const⼆;

             이것은 실제로 this라는 포인터를 상수로 선언한 것이다. 이것은 'this 포
          인터'에서 상세히 설명할 것이다.

처럼 명시한다. 이제 Print()함수의 내부에서 멤버 변수를 변경하는 어떠한 코
드도 컴파일 시간에 에러로 보고(report)된다. 아래의 예를 보자.

#include <stdio.h>

int i=1;

const int j=2;

const int* ip;

const char *s="Do not touch me!";//*s is constant

char* const t="Do not see me!";

class CTest {

    int i;

  public:

    CTest() { i=3; }

    const int* Test(const int c) const {

        //c=c+1;//이것은 에러이다.

        //i=i+1;//이 문장은 에러이다. i는 비록 상수형이 아니지만, Test()

                //함수가 상수 함수이므로, 이 함수내부에서는 어떠한 다른

                //멤버 변수도 바꿀 수 없다.

        printf("%d\n",c);

        return &i;

    }//Test()

};//class CTest

void main() {

    *(t+1)='O';

    printf("%s\n",s);

    printf("%s\n",t);

    CTest t;

    ip=t.Test(j);

}//main()

  실행 결과는

    Do not touch me!

    DO not see me!

    2

이다.

변덕장이 mutable

  상수 멤버 함수에서 부득이 어떤 변수 값을 변경해야 한다면, 그 변수는
mutable로 선언해야 한다. 즉, 상수 멤버 함수에서 mutable 변수를 변경하는
것은 불법이 아니다. 아래의 예를 보라.

#include <stdio.h>

class CTest {

    mutable int i;

  public:

    CTest() { i=1; }

    void Test() const {

        i=2;//i는 mutable 이므로 에러가 아니다.

        printf("%d\n",i);

    }

};//class CTest

void main() {

    CTest t;

    t.Test();

}//main()

  실행 결과는

2

이다(위의 소스는 Borland C++ 4.5이하에서 컴파일되지 않는다. mutable은
1997년 ISO 표준에 추가된 키워드이므로, Borland C++ 5.0이상, Visual C++
4.2이상의 버전에서 지원한다. 아래에서 설명할 explicit도 마찬가지이다. 위의
예는 Visual C++ 6.0에서 실행한 것이다). 실제 위 소스의 자세한 분석은
'this 포인터'에서 설명할 것이다.

   explicit

  explicit은 또 다른 C++의 주제이다. 이것은 생성자(constructor)의 함축적
변환(implicit conversion)을 막는다. 이 키워드가 생성자에 명시되면, 생성
자는 반드시 명시적 호출(explicit call) 되어야 한다. 아래의 예를 보자. 아
래의 예는 Visual C++ 6.0에서 실행한 것이다.

#include <stdio.h>

class CTest {

    int i;

public:

    CTest(int ti) { i=ti; }

    void Show() { printf("%d\n",i); }

};//class CTest

void main() {

    CTest t=1;//이 문장은 CTest t(1)로 함축적 변환된다.

    t.Show();

}

  실행 결과는

1

이다. 위의 소스에서

CTest t=1;

은 실제로

CTest t(1);

처럼 적어야 한다. 이것은 클래스의 생성자를 호출한다. 하지만, t=1;이란 문장
은 컴파일러에 의해 t(1);로 변환된다. 이것을 함축적 변환(implicit
conversion)이라 한다. 이것은 때때로 문서화에 방해가 된다.

  생성자에 explicit을 명시하면, 이러한 함축적 변환은 에러로 보고된다. 반드
시 명시적으로 생성자를 호출해야 한다. 아래의 예를 보자.

#include <stdio.h>

class CTest {

    int i;

public:

    explicit CTest(int ti) { i=ti; }

    void Show() { printf("%d\n",i); }

};//class CTest

void main() {

    CTest t=1;//이 문장에서 에러가 발생한다. 이 문장은 이제 함축적 변환

              //되지 않는다.

    t.Show();

}

  위의 소스는 컴파일 시간에(Visual C++ 6.0의 경우)

No constructor could take the source type, or constructor overload
resolution was ambiguous

에러 메시지를 출력한다.

   호출관례(calling conventions)

  함수의 호출 관례(function calling convention)란, 함수의 파라미터를 스
택(stack)에 푸시(push)하는 순서, 푸시하는 쪽 및 이름 변화를 명시한 것이
다. 아래의 테이블은 호출관례를 정리한 것이다.

변경자		푸시 순서	팝하는 쪽	이름 변화

__cdecl		Right first	Caller		'_' prepended

__fastcall		Left first		Callee		'@' prepended

__pascal		Left first		Callee		Uppercase

__stdcall		Right first	Callee		No change

  Right first는 함수의 파라미터를 오른쪽에서 왼쪽으로 평가하여 스택에 푸
시하는 것을 나타낸다. Caller/Callee는 호출하는 쪽/호출 당하는 쪽에서 스택
동작을 하는 것을 의미한다. C 방식의 경우 명칭 앞에 언더스코어 _가 붙는
다. 레지스터 방식의 경우 @가 붙는다. Pascal 방식의 경우 모두 대문자로 바
뀌며, WIN32 방식의 경우 이름은 우리가 적은 그대로 이다.

  Windows의 API(Application Programming Interface) 함수는 모두 WIN32
방식을 사용한다. 이것은 파라미터를 팝하는 쪽이 Callee이기 때문인데, 필자
는 그 이유에 대해서는 자세히 알지 못한다(아마도 Windows 운영체제에 이
미 존재하는 함수들에 대해서 일관된 파라미터 팝 방식을 적용함으로써 얻어
지는 최적화의 이점 때문인 것 같다. 만약 호출하는 쪽에서 파라미터를 팝 해
야 한다면, 실행 코드를 생성할 때 컴파일러는 이 부분의 코드를 생성해야 한
다. 컴파일러의 부담이 커지는 것(실행 파일의 크기 증가)은 둘째치고, 운영체
제의 관할 영역을 건드리는 보호(protection) 차원에서 문제가 될 수 있다).

  모든 C++ 컴파일러에서 기본값은 cdecl이다. 물론 기본 설정을 변경할 수
있다. 아래의 그림은 볼런드 C++ 3.1 및 비주얼 C++ 6.0에서 기본 호출 관례
를 설정하는 그림이다.

             볼런드 C++ 3.1의 Prefix/Suffix 코드 대화상자: 디폴트 호출 관례를 설정할
          수 있다. Prefix/Suffix를 Entry/Exit으로 표시하고 있다.

             비주얼 C++ 6.0의 프로젝트 설정 대화상자: Project->Settings의 C/C++ 탭
          에서 호출 관례를 설정할 수 있다. *는 디폴트를 나타낸다.

  비주얼 C++에서 Windows 프로그래밍을 하더라도 기본 설정은 __cdecl임을
주의하라. 이것은 독자들이 작성하는 콜백 함수(callback function)에 명시적
으로 __stdcall 즉 WIN32 혹은 WINAPI■를 지정해야 하는 이유이다.

            WIN32등은 windows.h에 선언된 __stdcall의 매크로이다. WIN16에서 이것은
          __pascal이었다. WIN32에서 __pascal은 __stdcall로 대치되었다.

  Windows 응용 프로그램의 시작 함수인 WinMain()은 __stdcall이지만,
WIN32 콘솔 응용 프로그램에서 시작 함수인 main()은 여전히 __cdecl이다.
아래의 예를 보자. main()에 cdecl 방식을 의미하는 _가 붙어 있다.

             Visual C++에서 main()함수 없이 링크했을 때의 에러: cdecl형식의 main()
          앞에 _가 붙은 것을 확인할 수 있다. 대부분의 컴파일러에서 함수이름이나
          변수 이름 앞에 _가 붙는 것은 이와 같은 이유때문이다.

  사소하지만, 주의가 필요한 호출 관례에 대한 예를 살펴보자. 아래 프로그램
을 볼런드 C++ 3.1에서 실행해 보자. 결과가 얼마일까?

#include <iostream.h>

void __pascal f(int i,int j) {

    cout << i << " " << j << endl;

}

void __cdecl g(int i,int j) {

    cout << i << " " << j << endl;

}

void main() {

    int i,j;

    i=1;j=2;

    f(i==j,i=j);//left to right evaluation

    i=1;j=2;

    g(i==j,i=j);//right to left evaluation

}

  실행 결과는

0,2

1,2

이다. pascal 방식은 파라미터 푸시를 왼쪽에서 오른쪽으로 한다는 사실을 주
의하라. 그러므로 f(i==j,i=j); 에서 i==j가 먼저 평가되었다. 이것은 거짓이므로

0

이다. i=j가 다음으로 평가되는데 이것은 i를 j값(2)로 만들고 식의 결과는

2

이다. g()호출에서 파라미터는 오른쪽에서 왼쪽으로 평가되므로, 결과가

1,2

인 것은 당연하다.

  위의 예를 비주얼 C++ 6.0에서 실행하면, 컴파일 시간에러가 발생한다.
Windows에서 더 이상 pascal 방식은 존재하지 않는다. 이것은 __stdcall로 대
치되었다.

nonstandard extension used : '_pascal' is an obsolete keyword

  __pascal을 __fastcall로 바꾸어 실행해 보자. 이상하게도 결과는

1,2

1,2

이다. 왜 f()의 호출이 0,2가 아닐까? 이것은 부효과(side effect)를 막기 위한
비주얼 C++ 설계자의 배려인 것 같다. 함수의 파라미터의 식(exprression)은
왼쪽에서 오른쪽으로 먼저 평가된 후, 이 결과를 순서에 맞게 스택에 푸시하
는 것 같다(이것은 확인된 바 없는, 필자의 생각이다).

   선언의 예

  지금까지 변수 선언에 사용된 모든 예를 종합적으로 살펴보자.

선언 문법		name의 형		예

type name;		type			int count;

type name[];		(open) array of type	int count[];

type name[3];		Fixed array of three	int count[3];

            elements, all of type

            (name[0], name[1], and

            name[2])

type *name;		Pointer to type		int *count;

type *name[];		(open) array of pointers	int *count[];

            to type

type *(name[]);		Same as above		int *(count[]);

type (*name)[];		Pointer to an (open)	int (*count) [];

            array of type

type &name;		Reference to type	int &count;

            (C++ only)

type name();		Function returning type	int count();

type *name();		Function returning	int *count();

            pointer to type

type *(name());		Same as above		int *(count());

type (*name)();		Pointer to function	int (*count) ();

            returning type

  실습문제

1.


30. 함수 이름 오버로딩

30. 함수 이름 오버로딩(f
unction name overloadi
ng)

  오버로딩은 OOL(object oriented language)의 특징을 결정짓는 3가지 특성
중의 한 가지이다. 추상화(abstraction)는 클래스가 디자인 될 때, 내부적인
자세한 사항은 숨겨져서, 클래스의 사용자가 내부적인 사항은 모르더라도 공
개된 멤버(public member)만을 사용하여 클래스를 사용할 수 있는 것을 의미
한다. 캡슐화(encapsulation)는 추상화의 동전의 양면이다. 추상화가 클래스
를 디자인하는 사람의 관점에서 클래스를 바라본 것이라면, 캡슐화는 클래스
를 사용하는 사람의 관점에서 클래스를 바라본 것이다. 클래스 디자이너는 클
래스의 자세한 사항을 숨겨야 하며, 클래스 사용자는 공개된 멤버만을 사용할
수 있으므로, 외부적인 요인에 의해서 클래스가 변경되거나 파괴되지 않는다
는 보장이 생긴다. 외부에서 보면 클래스는 단단한 캡슐에 쌓여져 있다.

  추상화와 캡슐화는 클래스와 클래스의 접근 변경자(access modifier)1)에

  1) private, public과 protected가 그것이다. 이 세 개의 키워드는 사용된

    위치에 따라, 접근 변경자, 접근 지정자(access specifier) 혹은 접근

    평가자(access qualifier)로도 불린다.

 의해 구현된다.

  그림 31-1	추상화: 클래스를 설계할 때, 복잡한 부분은 숨겨서 클래스 사용자는 클래

    스의 자세한 부분을 모르고도 클래스를 사용할 수 있어야 한다. C에서는 함수만이 추

    상화되었다. C++은 클래스를 통해 보다 높은 수준의 추상화를 제공한다.

  그림 30-2	캡슐화: 클래스 사용자는 클래스의 비공개 멤버를 접근할 수 없으므로, 객

    체는 보호된다.

  다형성(polymorphism)은 OOL의 두 번째 특성이다. 이것은 실세계의 다양
한 모양이 프로그래밍 언어에서도 존재함을 의미한다. 다형성은 문맥(context)
이 다르다면, 같은 이름의 함수나 연산자가 다른 기능을 하는 것을 허락한다.
최근에 추가된 이름공간(namespace)은 변수 이름의 다형성을 허락한다. 독
자들은 C에서처럼 같은 일을 하지만, 파라미터 형이 달라서 두 개의 함수를
작성해야 했던 수고를 이제 하지 않아도 된다.

  그림 30-3	다형성: 필요한 함수나 연산자는 함수나 연산자가 사용된 문맥에 따라 적절

    히 호출된다.

  C++의 템플릿(template) 기능은 다형성이 얼마나 유용한가를 잘 보여준다.

  상속성(inheritance)은 OOL의 세 번째 특성이다. 상속성의 의미는 어떤 클
래스가 디자인되었을 때, 이 클래스가 다른 클래스를 만들기 위해 사용될 수
있다는 것을 의미한다. 이것은 비슷한 일을 하지만, 다른 일을 하는 많은 응용
에 좋은 모델이다. 상속성이 코드의 중복성을 줄이기 위한 것은 아니지만, 실
제로 코드의 중복성은 사라진다. 상속성 자체는 엄청나게 중요한 하나의 모델
링 도구(modeling tool)이다.

  그림 30-4	상속성: Derived 클래스는 Base의 특성을 이어 받는다. Derived는 Base이다

    (Derived is-a Base). 하지만 Base가 Derived는 아니다. 상속성은 IS-A로 표현된다.

  이 장에서는 함수 오버로딩에 대해서 알아보자. 연산자 오버로딩은 다음장
의 주제이다.

  함수 오버로딩이 필요한가?

  왜 함수 오버로딩이 필요한가? 독자들은 자신의 프로그램을 만드는데, 두
수 중 큰 값을 구하는 max()라는 함수가 빈번히 필요할 것이다. 이 함수는 두
개의 정수(integer)중 큰 값을 구한다고 하자. 아마도 독자들은 다음과 같은
함수를 작성할 것이다.

int max( int i1, int i2 ) {

    return ( i1 > i2 ) ? i1 : i2;

}

  위의 함수는 훌륭하게 동작할 것이다. 하지만, 두 개의 double형 변수 중 큰
값을 구하는 함수가 또 필요하다고 하자. 독자들은 아마도 max2라는 함수를
만들 것이다. 어떤 독자는 정수의 큰 값을 구하는 함수는 MaxInteger(), 그리
고 지금 만들 함수는 MaxDouble()이라고 작성할지도 모른다.

double max2( double d1, double d2 ) {

    return ( d1 > d2 ) ? d1 : d2;

}

  문제는 여기에 있다.

"max()와 max2()는 같은 일을 한다. 하지만 왜 함수의 이름이 달라야
하는가?"

  이 문제에 대한 대답은 너무나 명확하다. 컴파일러는 유일한 함수 이름을
요구한다. max()라는 함수가 2개 존재한다면, 사용자가 어떤 함수를 호출하기
를 원했는지 어떻게 알 것인가? 하지만 C++은 이 문제에 대한 대답을 다음과
같이 내렸다.

"함수의 이름이 같더라도 주변의 문백(context) - 파라미터의 개수와 형 -
을 보면, 사용자가 호출을 원하는 함수가 어느 함수인지 결정할 수 있다."

  이것은 너무나 당연한 것 같지만, 컴파일러 입장에서는 심각하다. 이름이 같
은 두 개의 변수가 같은 범위(scope)에서 존재할 수 없듯이, 이름이 같은 두
개의 함수는 존재할 수 없다. 그러므로 컴파일러가 어떤 특별한 처리를 해 주
어야 한다. 아래의 예제는 함수 오버로딩을 사용한 적절한 예이다. max()라는
함수가 같은 이름으로 2개 존재한다. 하지만, 파라미터의 형이 다르므로,
main()에서 호출한 두 번의 max()는 적절한 max()를 호출할 것이다.

#include <stdio.h>

double max( double d1, double d2 ) {

    return ( d1 > d2 ) ? d1 : d2;

}

int max( int i1, int i2 ) {

    return ( i1 > i2 ) ? i1 : i2;

}

void main() {

    int    i = max( 12, 8 );//2번째 max()를 호출한다.

    double d = max( 32.9, 17.4 );//첫번째 max()를 호출한다.

    printf("%d,%g\n",i,d);

}

  이것은 너무나 큰 진보2)이다. 하지만, 독자들은 위의 소스 그대로가, 컴파일

    2) 우리가 당연하게 느끼는 많은 사실들이 당연하게 받아들여지기까지 얼

      마나 많은 시간이 걸리는가? 창조론이 그러하다.

러가 코드를 생성하는데 사용되지 않는다는 것을 알아야 한다. 컴파일러는 두
개의 max()를 적절히 호출하기 위해, 오버로드된 함수의 이름을 바꾸는 작업
을 수행하며, 이것은 이름 바꾸기(name mangling)로 알려져 있다. 컴파일
시간에 결정된 적절한 함수의 이름은 컴파일러에 의해 유일한(unique) 이름으
로 변경된다. 그대로는 아니겠지만, 아마도 위의 소스는 아래처럼 변경될 것이
다.

#include <stdio.h>

double maxB( double d1, double d2 ) {

    return ( d1 > d2 ) ? d1 : d2;

}

int maxA( int i1, int i2 ) {

    return ( i1 > i2 ) ? i1 : i2;

}

void main() {

    int    i = maxA( 12, 8 );//2번째 max()를 호출한다.

    double d = maxB( 32.9, 17.4 );//첫번째 max()를 호출한다.

    printf("%d,%g\n",i,d);

}

  모호함(ambiguity)

  함수가 오버로드되었을 때, 컴파일러가 구분할 수 없는 모호함이 발생한다
면, 컴파일 시간 에러가 발생한다. 예를 들면, max(int,int)와 max(int&,int&)
는 함수 호출 문맥으로 두 개의 함수를 구분할 수 없으므로, 에러이다. 또한
함수의 리턴형으로도 함수를 구분할 수 없으므로 에러이다.

    int Print();

    char Print();

함수 호출 문맥에서, 위 두 개의 함수를 구분할 방법은 없다. 왜냐하면, 모두
Print()처럼 사용하기 때문이다.

  const와 volatile 한정자(qualifier)에 의한 파라미터의 변경은 파라미터의 형
을 구분할 수 있으므로 가능하다. 아래의 예제를 참고하라.

#include <iostream.h>

class Over

{

public:

    Over() { cout << "Over default constructor\n"; }

    Over( Over &o ) { cout << "Over&\n"; }

    Over( const Over &co ) { cout << "const Over&\n"; }

    Over( volatile Over &vo ) { cout << "volatile Over&\n"; }

};

void main()

{

    Over o1;            // Calls default constructor.

    Over o2( o1 );      // Calls Over( Over& ).

    const Over o3;      // Calls default constructor.

    Over o4( o3 );      // Calls Over( const Over& ).

    volatile Over o5;   // Calls default constructor.

    Over o6( o5 );      // Calls Over( volatile Over& ).

}

  제한점

  ① 오버로드된 함수는 서로 다른 아규먼트 리스트를 가져야 한다.

  ② 함수의 리턴 형으로 오버로드된 함수를 구분하는 것은 불가능하다3).

    3) operator new만은 예외이다. new는 등호의 왼쪽(lvalue)이 반드시 명시되어야 하므

      로, 구분하는 것이 가능하므로, 어떤 컴파일러에서는 리턴형만이 다른 new의 오버로드

      를 허용한다. 각자가 사용하는 컴파일러의 도움말을 참고하라.

  독자들은 typedef가 새로운 형을 정의하지 않는다는 것에 주의해야 한다.

typedef char* CPtr;

이라고 선언했을 때, CPtr을 파라미터로 받는 test(CPtr)과 char *를 파라미터
로 받는 test(char *)는 오버로드할 수 없다. C++은 형의 이름(type name)으
로 형을 결정짓지 않는다. 내용(contents)으로 형을 결정한다.

  배열을 전달할 때 배열의 시작 주소만을 전달하므로, 다음과 같은 두 개의
함수 역시 오버로드가 불가능하다.

void Print( char *szToPrint );

void Print( char szToPrint[] );

  다른 범위(scope)에서의 오버로드

  함수를 오버로드할 때 클래스 범위(class scope)와 블락 범위(block
scope)가 엄격하게 지켜진다는데 유의하라. 즉 함수의 오버로드는 같은 범위
에서만 동작한다.

  상위 클래스에서 정의된 멤버 함수와 같은 이름으로 하위 클래스에서 함수
가 정의되었을 때, 상위 클래스의 함수는 무시되며4), 오버로드 되지 않는다.

    4) 이것을 오버라이딩(overriding)이라 한다.

 상위 클래스의 멤버 함수를 오버로드 하기 위해서는 이름 공간(name
space)을 위해 최근에 추가된 키워드 using을 사용해야 한다5).

    5) '이름 공간(name space)'를 참조하라.

  파일 범위에 선언된 함수가 블락 범위에 새로 선언되었다면, 블락 범위에서
파일 범위의 함수는 볼 수 없다. 아래의 예제를 참고하라.

#include <iostream.h>

void func( int i )

{

    cout << "Called file-scoped func : " << i << endl;

}

void func( char *sz )

{

    cout << "Called locally declared func : " << sz << endl;

}

void main()

{

    // Declare func local to main.

    extern void func( char *sz );

    func( 3 );    // Error. func( int ) is hidden.

    func( "s" );

}

  위의 예에서 extern 선언은 func()가 main에서 char *를 파라미터로 받는
유일한 함수임을 선언한 것이다. 그러므로, 첫 번째 함수는 타당한 호출이 아
니므로 컴파일 시간 에러가 발생한다.

  클래스의 멤버 함수를 오버로드할 때, private와 public 멤버 사이의 오버로
드 역시 가능하다. 아래의 예제는 비밀번호(password)를 사용하는 공개
(public) 멤버 함수 Deposit()과 비밀번호를 사용하지 않는 비공개 멤버 함수
Deposit()의 오버로드를 보여준다. 이러한 응용은 이러한 예에 아주 적절하다.

...

class Account

{

public:

    Account();

    double Deposit( double dAmount, char *szPassword );

private:

    double Deposit( double dAmount );

    int    Validate( char *szPassword );

};

void main()

{

    // Allocate a new object of type Account.

    Account *pAcct = new Account;

    // Deposit $57.22. Error: calls a private function.

    pAcct->Deposit( 57.22 );

    // Deposit $57.22 and supply a password. OK: calls a

    //  public function.

    pAcct->Deposit( 52.77, "pswd" );

}

  적절한 아규먼트를 가지는 함수 찾기

  오버로드된 함수를 결정하기 위하여, 컴파일러는 적절한(suitable) 함수를
찾는다. 적절한 함수는 다음의 순서로 결정된다6).

    6) 이 순서는 Visual C++ 5.0의 도움말을 참고로 한 것이다. 아마도 이 순서는 ANSI의

      표준 순서를 따르고 있을 것이다.

    ① 파라미터의 정확한 매치를 가지는 경우

    ② 간단한(trivial) 변환이 수행되는 경우

    ③ 정수의 승격(integral promotion7))이 이루어지는 경우

    7) 승격(promotion)은 형 변환(type conversion)의 한 종류이다. 같은 표기법을 사용하

      는 형 사이의 변환이 낮은 쪽에서 높은 쪽으로 일어난 경우, 이를 승격이라 한다. char

      가 int로 변환되는 경우가 이의 예이다.

    ④ 원하는 아규먼트의 형으로의 표준 변환(standard

       conversion)

    ⑤ 원하는 아규먼트 형으로의 사용자 정의 변환(user-defined

       conversion8))

    8) 변환 연산자 함수(conversion operator function) 혹은 생성자에 의한 변환을 의미한

      다.

    ⑥ 생략부호(ellipsis: ...)에 의해 표현되는 아규먼트를 가진 경우

  독자들은 위의 순서로도 발생하는 모호함에 대해 컴파일러가 어떤 조치를
취할지 의문이 생길지도 모른다. 여기에 관한 자세한 사항은 컴파일러의 도움
말을 참고하라. 모호함에 대한 강력한 해결 규칙은 다음과 같다.

"모호함이 발생하는 함수는 작성하지 말라."

  독자들은 Print(int)형의 함수를 선언해 놓고, Print(char)형을 사용하는 것을
즐겨할지 모른다. 이것은 얼마나 나쁜 습관인가? 비록 컴파일러에 의해 승격
(promotion)이 일어나더라도 이것은 좋은 습관이 아니다. Print(char)를 위해
하나의 함수를 더 작성하는 것이 바람직하다!

  예를 들어 다음과 같은 Print(int)함수가 있다고 하자.

void Print(int n) {

    //n을가지고 뭔가를 하는 함수의 본체

    ...

}

  독자들은 Print(char)를 위해 아래와 같이 함수를 작성할 수 있을 것이다.

void Print(char n) {

    Print((int)n);

    //추가도 해야 할 일들을 명시한다.

}

  놀라워 보이는 이러한 기교는 C++에서 클래스를 상속받아 설계할 때 일반
적인 일이다. 하위 클래스에서 상위 클래스의 가상 함수를 오버라이드
(override)할 때 대부분은 먼저 상위 클래스의 함수를 호출한다.

  멤버 함수의 오버로드

  클래스(class)를 모르는 독자는 이 부분을 건너뛰어도 좋다. 이 장에서 다룬
대부분의 내용은 일반 함수 - 멤버 함수가 아닌 - 의 오버로딩에 관한 것이
다. 하지만, 이 규칙은 클래스의 멤버 함수에 대해서도 같이 적용된다. 하지만
약간의 차이점이 존재한다. 이것은 this를 이해하는 것에서 시작한다. this는
static이 아닌 멤버 함수를 호출할 때 컴파일러에 의해서 자동으로 전달되는
객체 자신의 시작 포인터이다. 즉 멤버 함수가 static이면 this를 전달받지 않
는다. 여기에 관한 자세한 사항은 '연산자 오버로드'와 'this 포인터'에서 자세
히 다룬다.

  아래에 종합적인 예제를 참고하라. CPerson 클래스는 사람의 인적 사항 -
나이와 이름 - 을 관리한다. 나이를 설정하기 위해 SetAge()가 각각 정수(int)
와 포인터(char *)에 대해 오버로드되어 있다.

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

class CPerson {

    int age;

    char name[80];

public:

    CPerson(int a,char *n) {

        age=a;

        strcpy(name,n);

    }

    void SetAge(int a) {

        age=a;

    }

    void SetAge(char* a) {

        age=atoi(a);

    }

    void Print() {

        printf("%s, age=%d\n",name,age);

    }

};//class CPerson

void main() {

    CPerson a(29,"seojt");

    a.SetAge(30);

    a.Print();

}

출력결과는

seojt, age=30

이다.

  마지막으로 다루어야 할 주제는 오버로드된 함수의 주소에 관한 것이다. 이
것은 '함수 포인터(function pointer)'를 참조하라.

  실습문제

1. 다음 함수는 오버로드가 가능한가? 가능하다면 왜 그런가?

void Print( char szToPrint[] );

void Print( char szToPrint[][7] );

void Print( char szToPrint[][9][42] );

2. 오버로드된 함수의 주소를 결정하는 것은 왜 문제가 되지 않는가? 그 이유
를 설명하라(Hint: 함수의 주소를 얻기 위해서는 함수 포인터 변수를 선언해
야 한다).

3.


31. 기정의 파라미터

31. 기정의 인자(default
argument)

  독자들은 볼런드의 도스용 BGI 그래픽 함수1) 중에서

   1) 볼런드 5.0까지 BGI 라이브러리를 지원하고 있다. BGI(Borland Graphic Interface)는

    DOS 기반의 그래픽 프로그램을 개발할 때, 가끔 사용된 라이브러리였다. 하지만, 도스

    용 라이브러리는 교육용 외에는 별 의미가 없어졌다.

setlinestyle(<line style>, <user defined pattern>, <line width>);

을 알고 있는가? 이 함수는 선(line)의 모양과 굵기를 설정하는 함수인데, 첫
번째 파라미터에 따라 두 번째 파라미터는 사용이 되지 않기도 하는 기교를
사용한 함수이다. 만약 첫 번째 파라미터가 USERBIT_LINE2)이면 두 번째 파

   2) 볼런드의 도스용 그래픽 라이브러리 graphics.h에 선언된 상수이다.

라미터는 16비트의 사용자 정의 선 모양을 의미한다. 하지만, 첫 번째 파라미
터가 USERBIT_LINE이 아니면, 두 번째 파라미터 <user defined pattern>은
사용되지 않는다. 시스템에서 정의된 선 모양을 사용하기 때문에 필요가 없어
지는 것이다. 아마도 BGI의 설계자는

setlinestyle();

과

setuserlinestyle();

라는 두 개의 함수를 만들려고 했을지도 모른다. 하지만 함수의 일관성
(consistency)을 위해서, 별도의 두 개의 함수를 만들지 않고 한 개의 함수에
기능을 통합했을 것이다. 이것은 두 가지 면에서 이점이다.

    ① 함수의 일관성이 높아지므로, 사용자는 별도의 2개의 함수를 기억

       할 필요가 없다.

    ② 비슷한 일을 하는 2개의 함수를 위해서 별도의 코드를 작성해야

       하는 부담이 없어진다.

  아직까지 유용하게 사용되는 C 함수 strtok()3) 역시 마찬가지 기교를 사용

  3) 이 함수의 기능은 많은 응용에서 빈번하게 사용된다. 아마도 윈도

    우의 API중에서 SetProfileInt(), SetProfileString()등에서도 내부에서

    이 함수를 사용할 것이다. 이 함수는 문자열에서 사용자가 원하는 부

    분을 토큰(token)으로 끊어낸다.

하고 있다. strtok()는 두 번째 호출부터는 첫 번째 파라미터에 항상 NULL을
지정한다. 하지만 여전히 존재하는 문제는 여기에 있다.

"항상 일정한 값을 가지는 경우가 많은 파라미터의 값을 매번 명시해
주어야 하는가?"

  즉 C에서 위와 같은 기교의 사용이 편한 것은 사실이지만, 여전히 불편한
점이 남는다. setlinestyle()인 경우, 두 번째 파라미터가 사용되지 않더라도 파
라미터의 값을 명시해야하며, strtok()의 경우, 처음 호출을 제외하고는 항상
첫 번째 파라미터가 NULL이지만, 매번 NULL을 명시해 주어야 하는 것이다.

  C++에서 이러한 점은 좀 더 향상된 기교를 제공하면서 말끔하게 해결되었
다. 바로 기정의 파라미터(default parameter)를 사용하는 것이다!

  향상된 기교

  다음과 같은 Print()함수를 생각해 보자. 이 함수는 두 개의 파라미터를 받
는데, 첫 번째 파라미터는 출력하고자 하는 문자열의 시작 포인터이고, 두 번
째 파라미터는 문자열을 출력한 뒤 출력해야할 빈 줄의 수이다. 독자들은 아
마도 함수를 아래와 같이 작성할 것이다.

void Print(char* s, int nLine) {

    printf(s);

    for (int i=0;i<nLine;++i)

        printf("\r\n");

}

  만약 빈번하게 사용되는 형식이 Print("hello",1) 처럼 두 번째 파라미터가
대부분 1이라면, 독자들은 Print()의 오버로드된 새로운 버전의 함수를 작성할
지도 모른다.

void Print(char* s) {

    printf("%s\n"s);

}

  Print()를 오버로드 함으로써 Print("hello",1)을 사용하지 않고, Print("hello")
를 사용함으로써 같은 효과를 낼 수 있다. 아래의 예제를 참고하라.

#include <stdio.h>

void Print(char* s, int nLine) {

    printf(s);

    for (int i=0;i<nLine;++i)

        printf("\r\n");

}

void Print(char* s) {

    printf("%s\n",s);

}

void main() {

    Print("hello");

    Print("world",3);

}

  하지만 위의 방법이 최선의 해결책은 아닌 것 같다. main()에서 호출하는
입장에서는 깔끔하게 보이지만, 실제로 비슷한 일을 하는 Print()의 두 가지
버전이 존재하는 것은 불만이다. 실제로 main()의 첫 번째

Print("hello");

는

Print("hello",1);

과 동일한 효과를 가진다. 하지만 두 번째 1을 꼭 적어주어야 하는 것이다.

  기정의 파라미터는 이 상황에서 최선의 해결책이다. 기정의 파라미터는 함
수의 파라미터를 명시하지 않았을 때, 미리 정의된 파라미터의 값을 의미한다.
이것의 문법은 매우 단순하여, 변수를 선언하고 초기화하는 문법과 같다. 단,
파라미터이므로 함수의 헤더에 값을 명시해야 한다.

Print(char* s, int nLine=1);

  위의 함수 선언은 두 번째 파라미터의 디폴트 값(default value)이 1임을 의
미한다. 즉 두 번째 파라미터를 적지 않고

Print("hello");

라고 함수를 호출한 경우는

Print("hello",1);

과 동일하다. 디폴트 파라미터를 사용한 아래의 예제를 참고하라.

#include <stdio.h>

void Print(char *s, int nLine=1) {

    printf(s);

    for (int i=0;i<nLine;++i)

        printf("\r\n");

}

void main() {

    Print("hello");

    Print("world",3);

}

디폴트 파라미터를 사용할 때, 아래의 사항들을 주의해야 한다.

    ⼆ 기정의 파라미터는 함수의 호출에서 끝 부분 파라미터(trailing

       arguments)만이 생략될 수 있다. 그러므로 아래의 예는 잘못이다.

Print(char* s="", int nLine);

    이것은 함수 호출에서 Print( ,3) 처럼 사용할 수 없음을 의미한다.

    ⼆ 기정의 파라미터 값은 한 번 정의된 후에 다시 정의되어서는 안

       된다. 그러므로 아래의 예는 잘못이다.

    void Print(char* s, int nLine=1);//함수 선언

    ...

    void main() {

        ...

    }

    ...

    void Print(char* s, int nLine=1) {//함수 정의

        ...

    }

    ⼆ 하지만 추가의 기정의 파라미터는 명시될 수 있다. 아래의 예는

       타당하다.

    void Print(char* s, int nLine=1);//함수 선언

    ...

    void main() {

        ...

    }

    ...

    void Print(char* s=NULL, int nLine) {//함수 정의

        ...

    }

    ⼆ 디폴트 파라미터는 함수 포인터(function pointer)에 명시될 수 있

       다. 예를 들면 아래와 같다.

    int (*pShowIntVal)( int i = 0 );

    이것은 정수(int)를 리턴하고 정수 1개를 파라미터로 받는 함수의 포

    인터를 선언한 것인데, 이 포인터를 사용해서 함수를 호출하는 경우,

    첫 번째 파라미터가 명시되지 않았을 때, 0임을 의미한다.

  기정의 파라미터의 표현식(expression)

  디폴트 파라미터를 초기화하기 위한 표현식이 대부분 상수 표현식이지만,
항상 그런 것은 아니다. 표현식은 현재 범위에서 볼 수 있는 함수, 상수 표현
식, 전역 변수 등이 될 수 있다. 하지만, 표현식이 지역 변수(local variable)나
비정적 클래스 멤버 변수를 포함할 수는 없다. 아래의 예제를 참고하라.

BOOL CreateVScrollBar( HWND hWnd, short nWidth =

                       GetSystemMetrics( SM_CXVSCROLL ) );

  위의 함수는 Win95에서 주어진 폭(width)으로 수직 스크롤 바를 만든다. 만
약 폭이 명시되지 않으면, 현재 설정된 스크롤 바의 폭을 얻기 위해, 윈도우의
API함수인 GetSystemMetrics()를 호출한다.

  디폴트 표현식은 함수 호출 직후에 평가된다. 하지만, 실제적인 함수 호출이
일어나기 전에 표현식은 완전히 평가가 끝이 난다4).

  4) C에서 파라미터를 스택에 푸시하는 일은 불리는 함수(callee)가 한다. 디폴트 표현식

    의 평가는 파라미터가 스택에 푸시되고 나서, 함수의 몸체가 실행되기 전에 실행된다.

    그러므로 디폴트 표현식에 지역변수를 사용하는 것은 범위 규칙에 의해 지역 변수가

    먼저 적용되므로 에러이다.

  디폴트 표현식이 평가되는 시점이 함수 범위(function scope)에서이므로, 형
식 파라미터(formal parameter)나 지역 변수(local variable)를 사용하는 것은
허용되지 않는다.

  디폴트 파라미터가 쓰이기 전에 선언된 형식 파라미터가 전역 변수의 이름
을 숨김5)에 주의하라. 이것은 에러이다. 아래의 코드는 이것을 설명한다.

  5) 이것은 범위 규칙(scope rule)에 의해 명확하다. 지역 변수는 전역 변수와 같은 이름

    을 가질 수 있다. 하지만, 그러한 경우 지역 변수 - 그 변수를 둘러싼 가장 가까운 블

    락 - 가 전역 변수보다 먼저 보인다.

const int Categories = 9;

void EnumCategories( char *Categories[], int n = Categories );

  위의 예제에서 전역 변수 Categories는 함수 EnumCategories()내에서 범위
규칙에 의해 숨겨지므로(파라미터에 Categories가 있으므로), 위의 문장은 에
러이다.

  다른 고려사항

    ⼆ 디폴트 파라미터가 함수의 형태를 결정짓기 위해 사용될 수는 없

      다. 그러므로 디폴트 값만 다르고 다른 것은 모두 같은 두 개의 함

      수는 오버로드가 아니라 여러 번 정의한 것이 되므로 에러이다.

    ...

    void ShowIt(int k, int l=0);

    ...

    void ShowIt(int k, int l=20);//에러가 발생한다.

    ...

    ⼆ 디폴트 파라미터가 오버로드된 연산자에 적용되지는 않는다.

  디폴트 생성자에 사용된 디폴트 파라미터

  디폴트 파라미터와 관련하여 살펴보아야 할 다른 주제는 클래스와 관계된
것이다. 클래스의 멤버 함수들 역시 디폴트 값을 가질 수 있는 것은 당연하다
■. 함수 포인터를 선언할 때 디폴트 값을 가지도록 선언할 수도 있는데 이것

  멤버 함수(member function)가 일반 함수(function)는 아니지만, 함수의 특징을 가지고 있

    다. 일반 함수처럼 오버로딩 될 수 있으며, 이것이 일반적이다.

은 '함수 포인터'를 참고하라. 또한 템플릿(template)의 파라미터 역시 디폴트
값을 가질 수 있는데 이 부분은 '메타 클래스'를 참고하라. 클래스의 멤버 함
수를 오버라이딩 하는 것은 앞으로의 예들에서 빈번하게 사용될 것이다. 이것
은 얼마나 강력한 기능인가!

  클래스를 설계할 때 파라미터가 없는 생성자(constructor)를 정의할 수 있으
며, 이러한 생성자는 객체를 만들 때, 파라미터가 없어도 호출되므로, 디폴트
생성자(default constructor)라 한다. 하지만, 파라미터를 가지는 생성자가
디폴트 생성자가 될 수 있는데, 파라미터 모두가 디폴트 값을 가지는 생성자
가 바로 그러하다.

  디폴트 생성자는 의미가 명확하여 이해하기 힘든 것은 아니지만, 중요한 응
용 분야를 가진다. 이러한 응용 분야의 예로 다음과 같은 것이 있다.

    ■ 직렬화(serialization■)에서 반드시 필요한 디폴트 생성자

  직렬화는 클래스가 객체의 데이터를 저장할 수 있는 능력이다. 이것은 데이터베이스나

    분산 시스템(distributed system)에서 동기화되지 않은 질문(query)이나 프로세스가 처리

    되는 것이 결국은 순서적으로(serial) 처리된다는 개념과는 다른 것이다. Win95에서

    MFC를 이용하여 프로그램하거나 OWL로 프로그램하면 베이스 클래스에서 직렬화를

    제공한다. 이 교재에서는 직렬화에 대해서는 다루지 않는다.

  이러한 예들은 접어두고, 여기서는 디폴트 파라미터를 가지는 디폴트 생성
자에 대해서만 알아보도록 하자. 이것은 Win95를 포함한 대부분의 개발 환경
에서 너무나 빈번하게 사용되는 기교이다. 반드시 익혀두어 자신의 응용에 사
용할 수 있도록 하자.

#include <stdio.h>

class CRect {

    int left,top,right,bottom;

public:

    CRect(int l=0,int t=0,int r=0,int b=0) :

    left(l), top(t), right(r), bottom(b) {

    }//CRect

    //int Draw();

};//class CRect

void main() {

    CRect r;//디폴트 생성자를 호출한다. 디폴트 값이 생략된 것으로 간주하

            //므로 가능하다.

    CRect r2(0,0,100,100);

}

  위의 예에서 CRect()는 파라미터를 가지지 않는 디폴트 생성자가 없음에도
불구하고 존재한다. 왜냐하면, 디폴트 파라미터를 가지는 생성자가 존재하기
때문이다.

  실제로 main()에서 첫 번째 문장은 CRect의 디폴트 생성자를 호출해야 한
다. 이것은 디폴트를 모두 가지는 생성자 CRect(0,0,0,0)을 호출하므로 타당하
며, 이렇게 파라미터가 모두 디폴트 값을 가지는 생성자는 디폴트 생성자로
동작한다. 그러므로 위의 CRect() 클래스에 CRect() { ... }를 오버라이딩하는
것은 불가능하다.

  실습문제

1.


32. 참조와 별명

32. 참조(reference)와 별
명(alias)

  C++에서 새로운 의미를 지니게 된 &(ampersand)의 역할을 살펴보기 전에,
C에서 &의 역할(주소 연산자)을 살펴보는 것이 필요하다. 이것은 '&가 있어
야 하는가?'를 참조하라.

  이 장에서 우리가 살펴볼 &에 관한 주제들은 다음과 같다.

    ■ 별명(alias)

    ■ 참조에 의한 호출(call by reference)

    ■ 참조의 초기화

    ■ 참조를 리턴하는 함수

  별명(alias)

  이전의 장에서 필자의 별명(nickname)이 '택스'라고 언급한 바 있다. 필자의
이름이 비록 '진택'이기는 하지만, 누군가가 나를 '택스'라고 해도, 그 이름이
나를 부른다는 것을 나는 알고 있다. 이것은 별명이 이름과 동일한 기능을 하
는 것을 의미한다. 비록 별명(nickname)과 별도의 이름(alias)이 다르기는
해도, alias는 별명이라 번역한다.

  C++에서는 변수의 별명(alias) - 별명은 C++에서 참조(reference)이다 -
을 허락한다. 이것은 이름을 가지는 어떤 변수를 다른 이름 - 즉 별명 - 으로
접근(read/write)할 수 있음을 의미한다. 이것은 얼마나 헷갈리는 프로그래밍
개념인가! 필자는 '별명'을 비판한 글을 어떤 뉴스 그룹(news group)에서 읽
어보고 공감을 한 바 있다. 결론부터 말하면 다음과 같다.

"별명과 참조는 부득이한 경우를 제외하곤 사용하지 말라."

  별명은 그 특이성으로 인해, 선언하면서 초기화되어야 한다. 그렇지 않은 경
우에는 특별한 방법의 초기화 문법■을 사용해야 한다.

             함수형 초기화에서 자세히 다룬다.

  별명을 선언하는 방법은 새로운 연산자 &를 사용한다. 예를 들어 이미 선
언된 변수 i의 별명으로 iAlias를 선언하려면,

int& iAlias=i;

라고 한다. 독자들이 i를 1증가시키기 위해

++i;

를 사용하려고 한다면, 이것은 이제

++iAlias;

와 동일하다. 즉 iAlias에 가해지는 조작은 i에 가해지는 조작과 같다. 아래의
예제를 참고하라.

#include <iostream.h>

#include <stdio.h>

void main() {

    int i=0;

    int& iAlias=i;

    ++iAlias;

    cout << i << endl;

}

  위의 예제에서 i를 전혀 건드리지 않았지만, i의 값이 변화했음에 주목하라.
결과는

1

이다.

  참조를 초기화하지 않고 후에 초기화하려는 것은 에러이다. 아래의 예제를
컴파일하면,

"reference isn't initialized"

라는 에러가 발생한다.

#include <iostream.h>

#include <stdio.h>

void main() {

    int i=0;

    int& iAlias;//이 문장에서 에러가 발생한다.

    iAlias=i;//이 문장은 단순한 대입문으로 별명을 초기화하는 문장이 아니

             //다.

    ++iAlias;

    cout << i << endl;

}

  참조가 이미 선언된 변수에만 가능하다면, 이것이 무슨 소용인가? 즉 위의
예에서 i의 별명 iAlias를 선언하지 않고

int iAlias;

처럼 선언해서 사용하면 되지 않는가? 그렇다.

  그렇다면 독자들은 함수 안에서 선언된 별명에서 답을 찾으려고 할 지 모른
다. 아래의 예제를 참고하라.

#include <iostream.h>

#include <stdio.h>

void f(int i) {

    int& iAlias=i;

    ++iAlias;

    cout << i << endl;

}

void main() {

    int i=0;

    f(i);

    cout << i << endl;

}

  함수 f()에서 파라미터 i의 별명을 선언해서 사용하고 있다. 이 프로그램의
출력 결과는

1

0

이다. 즉 별명이 블락 범위(block scope)를 가지기 때문에, f()를 호출하는 쪽
에는 영향을 미치지 않는다.

  이 문제를 더 깊이 다루어 보기 전에 컴파일러가 별명을 어떻게 번역하는지
살펴보자. 이것은 쉽게 혼동될 수 있는 별명의 개념을 독자들에게 명확히 잡
아 줄 것이다.

별명이 번역되는 원리

  다시 첫 번째 예제를 고려해 보자. 먼저, 첫 번째 예제의 C버전을 작성해
보자.

#include <iostream.h>

#include <stdio.h>

void main() {

    int i=0;

    int* iAlias=&i;

    ++(*iAlias);

    cout << i << endl;

}

  위의 예제는 별명을 사용한 C++ 예제와 다른 것이 없다. 여전히 i를 건드리
지 않고도 i 값을 바꾸고 있다. 그렇다, 별명은 위의 코드처럼 번역된다. 사용
자가 선언한

int& iAlias=i;

라는 문장은

int* iAlias=&i;

로 번역하며,

++iAlias;

는

++(*iAlias);

로 번역한다.

  이 규칙은 별명이 복잡하게 사용되어 혼동되기 쉬운 상황에서, 확실한 별명
의 동작을 기억하는 유일한 방법이다. 요점은 다음과 같다.

"C++의 별명은 C의 포인터를 사용하여 적절히 번역된다."

  참조에 의한 호출(call by reference)

  &가 함수의 파라미터 선언 부분에 사용된 경우, 이 함수의 호출은 참조에
의한 호출(call by reference)이 된다. 참조에 의한 호출은 함수에 전달된 파라
미터의 값이 함수 안에서 변경된 경우, 함수를 호출한 쪽에 영향을 미치는 것
을 의미한다. 비록 C에서도 이러한 것이 가능했지만, C의 방법은 주소 연산자
&를 사용한 편법이었다. 그래서 C의 경우는 주소에 의한 호출(call by
address)라고 번역하기도 하지만, 엄격한 의미에서 값에 의한 호출(call by
value)과 같다. 단지, 부효과(side effect)로 변수의 값이 변경된 것뿐이다.

  파라미터로 전달된 두 정수를 교환(swap)하는 Swap()함수를 고려해 보자.

#include <iostream.h>

#include <stdio.h>

void Swap(int* i,int* j) {

    int t=*i;

    *i=*j;

    *j=t;

}

void main() {

    int i=2,j=3;

    Swap(&i,&j);

    cout << i << " " << j << endl;

}

  출력 결과는

3 2

이다.

  어떤 독자들은 위의 예에서 Swap(&i,&j)가 참조에 의한 호출이라고 생각할
것이다. 하지만, 그렇지 않다. Swap(&i,&j)은 단지 i와 j의 주소를 Swap()에
전달할 뿐이다. 또한 Swap()의 몸체(body)를 보라. 원하는 효과를 얻기 위해
서 복잡한 포인터 연산을 해 주어야 한다. 그러므로 Swap(&i,&j)은 단순한
값에 의한 호출일 뿐이다.

  위의 예제를 참조에 의한 호출을 사용하는 것으로 바꾸면 소스는 아래와 같
다.

#include <iostream.h>

#include <stdio.h>

void Swap(int& i,int& j) {

    int t=i;

    i=j;

    j=t;

}

void main() {

    int i=2,j=3;

    Swap(i,j);

    cout << i << " " << j << endl;

}

  결과는

3 2

이다. 위의 예에서 Swap()■의 머리에서 명시한 &를 생략하면 결과는 2 3이

             임시 변수 t를 사용하지 않고 변수를 Swap하는 방법에는 다음과 같은 기
          교가 있다. a=a+b; b=a-b; a=a-b; 이 그것이다. 이것보다 더 빠른 방법은
          a=a^b; b=a^b; a=a^b; 이다.

다. &의 유무에 따라 값에 의한 호출 혹은 참조에 의한 호출이 되는 것이다.
이것이 참조에 의한 호출의 핵심이다. 프로그래머가 신경 써 주어야 하는 것
은 C에서처럼 복잡한 포인터 연산이 아니라, &의 유무이다. 즉 컴파일러 수준
(level)에서 값의 교환이 결정되는 것이다. 하지만 C의 경우 프로그래머가 직
접 포인터 연산을 처리해야 하므로, 이것은 참조에 의한 호출이 아니다.

  그러므로 컴파일러는 함수의 머리에 &가 명시되면 변수의 값(value)을 전
달하는 것이 아니라, 변수의 참조(reference)■를 전달한다. 그리고 이 참조를

             이것을 포인터(pointer)라고 번역해서는 안 된다. 참조는 주소의 주소
          (pointer to pointer)도 포함하는 개념이다.

적절한 기계어 코드로 번역하는 것이다.

기우(imagined fears)

  참조가 강력한 것임에도 불구하고 명백한 단점이 있다. 필자는 이 단점 때
문에 참조를 즐겨 사용하지 않는다. C버전의 Swap()은

Swap(&i,&j);

를 사용함으로써 호출하는 쪽에서 i와 j의 값이 변경되어 돌아올 것이라는 기
대를 하게 된다. 하지만 C++버전은 값에 의한 호출과 참조에 의한 호출 모두
에

Swap(i,j);

를 사용한다. 즉 함수의 정의를 보지 않고는 i와 j가 어떠한 방식으로 호출되
는지 알 방법이 없다. 이것은 문서화(documentation)에 큰 방해가 된다. 해
결 방법이 문법적으로 존재하는 것은 아니지만, 함수의 이름을 붙이는 관례를
사용함으로써 이 문제를 어느 정도 해결할 수는 있다. 즉 함수의 이름에 명백
하게 파라미터가 참조임을 명시하는 것이다.

SwapRef(i,j);

  위 문장은 확실히 Swap(i,j)보다 명백하다. 하지만 문제를 근본적으로 해결
한 것은 아니다. 결론은 다음과 같다.

"참조를 사용하지 않으면, 굉장히 복잡한 포인터 연산을 여러 번 해야하는
경우를 제외하곤, 참조를 사용하지 말라. 부득이 참조를 사용해야 한다면,
그 함수가 참조 파라미터를 가짐을 함수의 이름으로 명시하라."

  참조의 초기화

  참조가 후에 초기화 될 수 없음에도 불구하고, 명백히 후에 초기화되어야
하는 상황이 발생한다. 클래스의 멤버로 참조 변수가 선언된 경우가 그러한
예이다. 이러한 초기화에 관한 자세한 사항은 '함수형 초기화'를 참조하라.

#include <iostream.h>

#include <stdio.h>

class CTest {

    int& alias;//클래스 정의 부에서는 초기화가 불가능하다.

public:

    CTest(int i) : alias(i) {}//생성자에서 참조를 반드시 초기화 해야한

                               //다.

    void Inc() { ++alias; }

};//class CTest

void main() {

    int x=0;

    CTest a(x);

    a.Inc();

    cout << x << endl;

}

  출력 결과는

1

이다.

  위의 예에서는 클래스의 멤버로 참조가 선언된 경우이다. 클래스는 형을 정
의하는 문장에 속하므로, 데이터 멤버를 초기화할 수 없다. 초기화하는 유일한
방법은 생성자에서 함수형 초기화를 사용하는 것이다.

  지역 변수 x가 객체 a에서 참조되므로, x에 대한 조작 없이 x값이 변경되
는 것은 다소 놀랄만하지만, 이것이 참조의 위력이다.

  참조를 리턴하는 함수

  참조가 가장 강력하게 사용되는 경우는 바로 함수의 리턴 형(return type)이
참조인 경우이다. 연산자 오버로딩(overloading)의 경우 참조 리턴(reference
return)은 빈번하며 중요하게 사용된다. 이 부분의 자세한 사항은 '연산자 오
버로딩'을 참고하라.

#include <iostream.h>

#include <stdio.h>

int odd[]={1,3,5,6,9};

int& Powerful(int i) {//참조를 리턴한다.

    return odd[i];

}

void main() {

    cout << Powerful(3) << endl;

    Powerful(3)=7;//이것은 얼마나 강력한가!

    cout << Powerful(3) << endl;

}

  결과는

6

7

이다. Powerful()함수가 참조를 리턴하므로, LValue가 될 수 있음을 의미한다.
그러므로, 등호의 왼쪽(left value)에 쓰일 수 있다.

진보된 주제

  멤버 함수가 참조를 리턴하는 경우와, 참조를 리턴하는 연산자 오버로딩에
관한 주제는 '연산자 오버로딩'에서 다룬다.

   아래의 프로그램은 왜 잘못인가?

int& Simple(int i) {

    int twoTime=2*i;

    return twoTime;

}

   twoTime은 스택에 할당된다. Simple()이 리턴되었을 때 twoTime은
메모리에 존재하지 않는다.

  실습문제

1. 참조가 참조될 수 있는가? 지금 당장 실험해 보라.

2.


34. new/delete: 진보된 주제

34. new/delete: 진보된
주제

  이 장의 주제는 모두 new/delete와 관계가 있다. 이 장의 주제는 다소 어려
운 부분이므로 건너뛰어도 좋다.

    ■ 함수 포인터 배열의 할당: new를 이용하여 함수 포인터의 배열을

    할당하는 법

    ■ new/delete의 오버로딩: 클래스의 멤버가 아닌 new/delete를 오버

    로딩하는 법

    ■ 동적 2차원 배열을 할당하는 3가지 방법

  함수 포인터 배열(array of pointers to
functions)의 할당

  '함수 포인터'에서 들었던 예를 다시 가져와 보자. 아래의 프로그램은 함수
포인터 배열 f를 사용하여 프로그램을 단순화시킨 예이다.

#include <stdio.h>

#include <math.h>

#include <conio.h>

long Exponent(long a,long b) {

    return pow(a,b);

}//Exponent

long Multiply(long a,long b) {

    return a*b;

}//Multiply

long Divide(long a,long b) {

    return a/b;

}//Divide

void main() {

    long (*f[])(long,long)={Exponent,Multiply,Divide};

    long a=5,b=2;

    int i;

    printf("a=5,b=2\n"

           "Select a Function Number:\n"

           "0=Exponent(a,b): a**b\n"

           "1=Multiply(a,b): a*b\n"

           "2=Divide(a,b): a/b\n");

    scanf("%d",&i);

    printf("%ld\n",(*f[i])(a,b));

}

  위의 예에서 f[]는 배열이다. 크기는 3이며 f[0],f[1]과 f[2]는 각각
Exponent(), Multiply()와 Divide() 함수의 시작 주소를 가리킨다. f[i]는 함수
의 시작 주소이므로 함수를 호출하기 위해서 (*f[i])()와 같은 문법을 사용한
것을 주의하라.

  정수 배열을 할당하기 위해

    int* ip;

    ip=new int[10];

처럼 사용하듯이 함수 포인터 배열을 할당하는 방법은 없을까?

  아래의 문장은 정수를 리턴하는 7개의 함수를 가리키는 포인터의 배열
(pointers to functions)을 할당한다.

int (**p) () = new (int (*[7]) ());

  먼저 포인터의 배열이므로 데이터 형은 **p처럼 선언되어야 한다(int **p).
또한 일반 포인터와 함수 포인터를 구분하기 위해 마지막에 ()를 사용하였으
며(int **p()), 함수 선언(function declaration)과 함수 포인터(pointer to
function)를 구분하기 위해 **p는 괄호로 묶여 있다(int (**p)()). 이러한 심벌
은 어느 것 하나 생략되어서는 안 된다.

  이제 오른쪽 new부분을 보자. 배열을 할당하므로 [7]을 사용했으며, 함수
포인터인 것을 알려주기 위해 마지막에 ()가 사용되었다.

new int[7]();

  함수 포인터임을 알려 주기 위해 *가 사용되었으며, 정수 포인터 배열과 함
수 포인터 배열을 구분하기 위해, 첨자 []와 *는 괄호로 묶여야 한다.

new int (*[7])()■;

             new int *[7](); 은 정수 포인터 배열을 할당하려 시도한다. 그러므로 이것
          은 함수 포인터의 배열로 타당하지 않다. new int[7];은 크기가 7인 정수 배
          열을 할당한다. 하지만, new int*[7];은 크기가 7인 정수 포인터 배열을 할당
          한다.

  마지막으로, new의 오른쪽을 구성하는 형의 문법적인 에러를 막기 위해 전
체가 괄호로 묶여야 한다. 이 괄호가 없으면, 컴파일 시간 문법 에러(syntax
error)가 발생한다. 완성된 문장은 아래와 같다.

int (**p) () = new (int (*[7]) ());

  함수 포인터 배열을 사용한 원래의 소스를 동적으로 메모리를 할당하는 예
로 바꾼 소스는 아래와 같다.

#include <stdio.h>

#include <math.h>

#include <conio.h>

long Exponent(long a,long b) {

    return pow(a,b);

}//Exponent

long Multiply(long a,long b) {

    return a*b;

}//Multiply

long Divide(long a,long b) {

    return a/b;

}//Divide

void main() {

    long (**f)(long,long);//={Exponent,Multiply,Divide};

    long a=5,b=2;

    int i;

    f=new (long (*[3])(long,long));//동적으로 메모리를 할당한다.

    f[0]=Exponent;

    f[1]=Multiply;

    f[2]=Divide;

    printf("a=5,b=2\n"

           "Select a Function Number:\n"

           "0=Exponent(a,b): a**b\n"

           "1=Multiply(a,b): a*b\n"

           "2=Divide(a,b): a/b\n");

    scanf("%d",&i);

    printf("%ld\n",(*f[i])(a,b));

    delete[] f;//동적으로 할당한 메모리를 해제한다.

}

   new/delete의 오버로딩

  new/delete는 유일하게 비 클래스의 멤버로(전역 함수로) 오버로딩 할 수
있는 연산자이다. 즉, new와 delete는 완전하게 - 특정 클래스와 상관없이 -
사용자가 재 정의하는 것이 가능하다.

  new/delete의 클래스 멤버로서의 오버로딩은 '연산자 오버로딩'에서 다룬다.

  그렇다면, 언제 전역으로 new와 delete를 오버로딩하는가? 이것은 어떤 특
정한 할당 방식을 사용하고자 할 때 많이 사용된다. 예를 들어, 힙 기억 장소
를 다 썼을 때 가상 기억 장치로 디스크 파일을 사용하여 자동적으로 시작하
는 할당 루틴이 필요할 수도 있을 것이다. 필자는 C++에 포함(embedded)된
데이터베이스 프로그램에서 전역으로 new/delete를 오버로드한 것을 본 적이
있다. 실제로 독자들이 비교적 큰 규모의 프로그램을 코딩하더라도
new/delete를 전역으로 오버로딩할 일은 거의 없을 것이다.

#include <iostream.h>

#ifdef __BCPLUSPLUS__

  #include <alloc.h>

#else

  #include <memory.h>

  #include <stdlib.h>

#endif

//typedef unsigned int size_t;

void *operator new(unsigned int dataSize,char filler) {

    void *rtn;

    if ((rtn=malloc(dataSize))!=NULL)

        memset(rtn,filler,dataSize);

    return rtn;

}//operator new

void operator delete(void *type) {

    free(type);

}//operator delete

void main() {

    char *cp=new('1') char[10];

    int i;

    for (i=0;i<10;++i)

        cout << cp[i];

    delete[] cp;

}

new/delete의 오버로딩: 배열 형식

  독자들은 delete p; 와 delete[] p;가 다르다는 것을 알고 있을 것이다.
delete[] p; 형식은 new를 사용하여 배열로 할당했을 때 사용해야 한다. 배열
로 할당했을 때 delete p; 형식을 사용하면, 메모리가 샐 수도 있다.

  컴파일러 구현자 입장에서 delete p; 와 delete[] p; 는 어떻게 다를까? 이것
은 아래의 진보된 주제에서 다룬다.

void* operator new[](size_t size) {

}

void operator delete[](void* p) {

}

  delete p; 와 delete[] p;는 어떻게 다른가?:
진보된 주제

단편화(fragmentation)

  메모리 할당을 시도했을 때 메모리 단편화(fragmentation)는 심각한 문제가
될 수 있다.

   동적 2차원 배열(1):

#include <stdio.h>

void main() {

    int (*ipp)[3];//not int* ipp[3];

    ipp=new int[2][3];

    delete[] ipp;

}

   동적 2차원 배열(2): 포인터의 포인터

#include <except.h>

#include <iostream.h>

int m = 3;//THE NUMBER OF ROWS.

int n = 5;//THE NUMBER OF COLUMNS.

void Display(long double **data) {

    for (int i = 0; i < m; i++) {

        for (int j = 0; j < n; j++)

            cout << data[i][j] << " ";

        cout << "\n" << endl;

    }//for

}//Display

void Deallocate(long double **data) {

    for (int i = 0; i < m;  i++)

        delete[] data[i];// STEP 1: DELETE THE COLUMNS

    delete[] data;// STEP 2: DELETE THE ROWS

}//Deallocate

int main(void) {

   long double **data;

   try {// TEST FOR EXCEPTIONS.

      data = new long double*[m];// STEP 1: SET UP THE ROWS.

      for (int j = 0; j < m; j++)

          data[j] = new long double[n];// STEP 2: SET UP THE COLUMNS

   }

   catch (xalloc) {// ENTER THIS BLOCK ONLY IF xalloc IS THROWN.

   // YOU COULD REQUEST OTHER ACTIONS BEFORE TERMINATING

      cout << "Could not allocate. Bye ...";

      exit(-1);

   }

   for (int i = 0; i < m; i++)

      for (int j = 0; j < n; j++)

          data[i][j] = i + j;// ARBITRARY INITIALIZATION

   Display(data);

   Deallocate(data);

   return 0;

}

결과는

0 1 2 3 4

1 2 3 4 5

2 3 4 5 6

이다.

   동적 2차원 배열(3): 연산자 오버로딩

#include <iostream.h>

class CInt {

    int* ip;

    int size;

public:

    CInt(int size=100);

    ~CInt();

    int& operator[](int i);

};//class CInt

CInt::CInt(int size) {

    ip=new int[size];

    CInt::size=size;

}

CInt::~CInt() {

    delete[] ip;

}

int& CInt::operator[](int i) {

    return ip[i];

}

void main() {

    CInt* pArray;

    pArray=new CInt[3];

    int i,j;

    for (i=0;i<3;++i) {

        for (j=0;j<100;++j) {

            pArray[i][j]=(i+1)*j;

            //pArray[i].operator[](j)=(i+1)*j;

        }

    }//for

    cout << sizeof(pArray) << endl;

    cout << sizeof(*pArray) << endl;

    delete[] pArray;

}

  실습문제

1.


35. 함수형 초기화
functional initialization

  생성자(constructor)

  클래스의 생성자는 객체가 메모리에 할당되고 나서 첫 번째로 불리는 함수
이다. 이 특별한 함수는 일반적으로 객체의 특정한 값들을 초기화하기 위해
사용된다. 또한, 생성자나 소멸자는 사용자가 - 함수를 호출하듯이 아무 때나
- 호출할 수 없으므로, 리턴 형(return type)을 가지지 않는다. 생성자를 사용
자가 호출 할 수 없음에도 불구하고, 생성자는 오버로드될 수도 있으며, 파라
미터를 가질 수도 있는데, 이것은 객체를 인스턴스화(instanciation) 할 때,
클래스의 사용자가 파라미터를 명시할 수 있기 때문이다. 이러한 클래스의 생
성자 때문에, 미리 정의된 형(predefined type)⼆에 대해서도 생성자 형태의

            사용자 정의 형을 제외한 int, float등의 기본형을 말한다.

초기화가 가능하다.

  독자들은 일반적인 정수(integer)의 기능을 확장하기 위해, 정수 클래스 Int
를 만들지도 모른다⼆. 이 클래스가 비공개 멤버로 정수 i를 가지고, 오버

            Java 같은 언어에서는 기본형을 확장한 이러한 클래스가 표준 라이브러리
          형태로 제공된다.

로드된 연산자 ++를 가지며, 현재 정수의 값을 읽는⼆ 함수인 Get을 가진다고

            멤버 변수를 읽는 일을 하는 멤버 함수를 리더(reader)라 한다. 쓴다면 라
          이터(writer)라 한다.

하자. Int는 단 1개의 생성자를 가지며 파라미터를 1개 받는다. 생성자는 멤버
변수 i를 사용자가 지정한 파라미터로 초기화하는 일을 한다. 독자들은 이 클
래스의 객체를 만들기 위해

Int k(5);

과 같이 사용할 수 있으며, 이것은 Int의 생성자를 호출하여, 객체 k의 멤버
변수 i를 3으로 초기화한다. 아래의 예제를 참고하라.

#include <stdio.h>

#include <iostream.h>

class Int {

    int i;

public:

    Int(int i=0) { Int::i⼆=i; }

            멤버 함수의 파라미터와 멤버 변수의 이름을 구분하기 위해 클래스 이름
          과 범위 해결사를 사용한다.

    int operator++() {

        ++i;

        return i;

    }

    int Get() { return i; }

};//class Int

void main() {

    Int i(5);

    ++i;

    cout << i.Get() << endl;

}//main

결과는

6

이다. 이러한 초기화가 클래스에 적용되었을 때는 전혀 어색하지 않다. 하지
만, 이러한 초기화 - 초기화에 괄호를 사용하는 것 - 가 기본형(basic type)에
대해서도 가능하다.

  독자들은 정수형 변수 k를 0으로 초기화하기 위해,

int k=0;

이란 문장을 사용할 것이다. 하지만, C++에서는

int k(0);

과 같은 형태의 초기화가 가능하며, 이것을 함수형 초기화라고 한다. 하지만,
int 가 클래스인 것은 아니다. 단지, 이런 초기화가 문법으로 정의되어 있을
뿐이다. 그러므로 int k(0)을 생성자를 호출한다고 생각해서는 안된다. 아래의
예제를 참고하라. main()에서 정수형 변수 t를 초기화하기 위해 함수형 초기
화를 사용하고 있다.

#include <stdio.h>

#include <iostream.h>

class Int {

    int i;

public:

    Int(int t=0) : i(t) {}

    int operator++() {

        ++i;

        return i;

    }

    int Get() { return i; }

};//class Int

void main() {

    Int i(5);

    int t(7);

    ++i;

    cout << i.Get() << endl;

    cout << t << endl;

}//main

결과는

6

7

이다.

  함수형 초기화가 별도의 장(chapter)으로 구분된 이유는 클래스의 멤버 변
수를 초기화하기 위해, 특별히 사용되는 경우와, 레퍼런스를 초기화하기 위해
반드시 사용되어야 하는 경우가 있기 때문이다.

  레퍼런스와 상수의 초기화

  레퍼런스가 C++ 프로그래머에게 많은 환영을 받고 있는 것은 아니지만⼆,

            레퍼런스를 사용하면, 함수를 호출하는 쪽의 문백으로 call by value 인지,
          call by reference 인지를 확인하는 방법이 없으므로, 오히려 혼돈이 된다.
          그래서 많은 C++ 프로그래머들이 여전히 주소를 전달하는 옛 C방식을 사
          용하고 있으며, 필자의 경우도 마찬가지이다. k=f(i); 라는 문장으로는 i가
          reference인지 value인지 확인할 방법이 없다.

여전히 중요하다. 레퍼런스가 블락 범위에서 선언된 지역 변수인 경우는 문제
가 되지는 않는다. 이러한 경우 레퍼런스는 반드시 초기화되어야 하며, 그렇지
않은 경우 컴파일 시간 에러이다.

    int i=3;

    int& refi=i;

    ++refi;

    cout << i;

    ...

  위 프로그램 조각(program segment)의 출력은 4이다. 두 번째 줄은 refi를 i
의 레퍼런스로 초기화하고 있는데, 이것을 분리하여 다음과 같이 선언하는 것
은 가능한가?

    int i=3;

    int& refi;

    refi=i;

    ...

불가능하다. 레퍼런스는 반드시 선언하면서 초기화가 되어야 한다. 그러므로
레퍼런스가 블락 변수로 사용될 일을 거의 없으며, 함수의 파라미터로 그리고
클래스의 멤버 변수로 사용되는 경우가 대부분이다.

  클래스의 멤버 변수로 사용되는 경우를 고려해 보자. CTest는 정수 레퍼런
스 멤버를 가진다. 이것은 비정적(nonstatic) 멤버 변수이므로 초기화할 수 없
으며, 생성자에 초기화를 코딩해야 한다. 그래서 아래처럼 CTest의 생성자를
코딩하였다. 이것은 가능한가?

class CTest {

    int& ref;

public:

    CTest(int &tmp) { ref=tmp; }

    void Set(int i) { ref=i; }

};//class CTest

  불가능하다. 블락 범위의 레퍼런스를 초기화하지 못하는 것처럼, 위의 소스
는 컴파일 시간 에러이다. 그러므로, 레퍼런스가 멤버 변수인 경우 레퍼런스를
초기화하는 특별한 문법이 필요하다. 그렇다. 바로 함수형 초기화이다! 소스는
아래와 같이 수정되어져야 한다.

class CTest {

    int& ref;

public:

    CTest(int &tmp): ref(tmp) {}

    void Set(int i) { ref=i; }

};//class CTest

  생성자 함수의 헤더와 블락 사이에 콜론(:)으로 구분하여, 함수형 초기화■

             클래스에 사용된 함수형 초기화의 정식 명칭은 멤버 초기화 리스트
          (member-initialization list)이다. 하지만, 이 책에서는 함수형 초기화라
          고 계속해서 사용한다.

를 적어주면 된다. 하지만, 이러한 초기화가 레퍼런스의 초기화에만 가능한 것
은 아니며, 일반 멤버 변수의 초기화도 이러한 방식으로 하는 것이 가능하다.
이것은 프로그래머의 취향이다. 하지만, 레퍼런스는 반드시 함수형 초기화를
해야한다.

  예를 들어, CTest의 정수 멤버 변수 k를 초기화하는 생성자는

    class CTest {

        int k;

    public:

        CTest(int tmp){k=tmp;}

    };//class CTest

가 가능하며 일반적이다. 하지만, 아래의 형식도 가능하다.

    class CTest {

        int k;

    public:

        CTest(int tmp):k(tmp){}

    };//class CTest

  아래의 예제를 참고하라. 레퍼런스의 특징 때문에 결과는 다소 놀랄만하다.

#include <stdio.h>

#include <iostream.h>

class CTest {

    int& ref;

public:

    CTest(int &tmp) : ref(tmp) {}

    void Set(int i) { ref=i; }

};//class CTest

void main() {

    int t=1;

    CTest obj(t);

    obj.Set(10);

    cout << t << endl;

}//main

결과는

10

이다.

  클래스의 멤버로 상수가 포함된 경우, 상수값을 초기화하는 유일한 방법 역
시 함수형 초기화를 사용하는 것이다. 상수 멤버에 대해 정적 변수(static
variable)를 초기화하듯이 할 수 없다. 아래의 예제를 참고하라.

#include <iostream.h>

#include <stdio.h>

class CTest {

    const int c;

    int i;

public:

    CTest(int t=0):c(100) {

        i=t;

    }

    void Print() {

        printf("i=%d\n",i);

        printf("c=%d\n",c);

    }

};//class CTest

void main() {

    CTest c;

    c.Print();

}//main

  결과는

    i=0

    c=100

이다. 위에서 c를 초기화하기 위해 클래스 안에서

class CTest {

    const int c=100;//error!

...

처럼 적는 것은 에러이다(만들어지지 않은 객체의 값을 어떻게 초기화할 수
있는가?). 또한 클래스의 외부에서 다음 문장처럼 초기화하는 것도 에러이다.

const int CTest::c=100;//이 문장은 정적 변수를 초기화하기 위해 사용한다.

...

  베이스 클래스의 생성자 호출과 멤버 객체의
초기화

  클래스의 함수형 초기화처럼 보이는 어떤 것들은 실제로 함수형 초기화가
아니며, 베이스 클래스의 생성자를 호출하는 경우가 있다. 이 부분은 '가상 함
수'에서 자세히 다루며, 여기서는 간단한 예를 한 개만 든다.

#include <stdio.h>

#include <iostream.h>

class A {

    int i;

public:

    A(int t){i=t;}

};//class A

class B : public A {

    int i;

public:

    B(int t,int u) : A(t), i(u) {}

};//class B

void main() {

}//main

  위의 예에서 A(t)는 베이스 클래스의 생성자 호출이다. 하지만, i(u)는 함수
형 초기화이다. A와 i는 같은 범위(scope)에서 보이므로, 이것이 애매함을 발
생하지는 않으므로, 컴파일러는 훌륭하게 이 둘을 구분한다.

  멤버 객체의 초기화

  어쩔 수 없이 함수형 초기화를 해야하는 마지막 경우는 클래스의 멤버로 다
른 클래스 객체를 가지는 경우이다.

//{{여기부터 내용을 적을 것

아래의 예제를 참고하라.

#include <iostream.h>

#include <stdio.h>

class CPoint {

    int x,y;

public:

    CPoint(int tx,int ty){ x=tx; y=ty; }

    void Print() {

        cout << x << "," << y;

    }

};

class CTest {

    CPoint p;

public:

    const int c;

    enum { A=100, B=200 };

    int i;

    static int s;

public:

    CTest(int t=0):c(10),p(3,4) {

        i=t;

    }

    void Print() {

        printf("i=%d\n",i);

        printf("c=%d\n",c);

        printf("s=%d\n",s);

        p.Print();

    }

};//class CTest

int CTest::s=20;

void main() {

    CTest c;

    c.Print();

    printf("%d\n",CTest::A);

}//main

  Pointer to Member operator: .* and ->*

#include <stdio.h>

#include <iostream.h>

//#pragma pointers_to_members(full_generality)

class CObject {

    int i;

public:

    CObject(int t=0):i(t) {}

    void Add(int k) { i+=k; }

    void Show() { cout << i << endl; }

};//class CObject

void (CObject::*pMemberFunc)(int)=CObject::Add;

void main() {

    CObject obj;

    //CObject* pObject=&obj;

    //pObject->pMemberFunc=&CObject::Add;

    //(pObject->*pMemberFunc)(3);

    //obj.pMemberFunc=CObject::Add;

    (obj.*pMemberFunc)(3);

    obj.Show();

}//main

#include <stdio.h>

#include <iostream.h>

class CObject {

    int i;

public:

    CObject(int t=0):i(t) {}

    void Add(int k) { i+=k; }

    void Show() { cout << i << endl; }

};//class CObject

void (CObject::*pMemberFunc)(int)=CObject::Add;

void main() {

    CObject obj1,obj2;

    CObject* pObject=&obj2;

    (pObject->*pMemberFunc)(3);

    pObject->Show();

}//main

  Error: Undeclared Identifier

#include <stdio.h>

#include <iostream.h>

class CObject {

    int i;

public:

    void (CObject::*pMemberFunc)(int);

public:

    CObject(int t=0):i(t) {}

    void Add(int k) { i+=k; }

    void Show() { cout << i << endl; }

};//class CObject

void main() {

    CObject obj1,obj2;

    CObject* pObject=&obj2;

    pObject->pMemberFunc=CObject::Add;

    (pObject->*pMemberFunc)(3);//error!

    pObject->Show();

}//main

  외워야 할 표(table)

  아래에 각 변수의 형태와 상황에 따른 초기화에 관한 표를 제시한다. 반드
시 익혀 숙지해 두기 바란다.

초기화 형태

변수 혹은 객체의 형

예

외부에서 한
번만 초기화

static

class C {

  static int i;

  ...

};

...

int C::i=100;

...

클래스를 정의할
때 초기화

enum

class C {

  enum {A=100,B=200};

};

...

cout << C::A;

...

함수형 초기화

const

class C {

  const int i;

public:

  C():i(100){}

...

};

reference

class C {

  int& i;

public:

  C(int k):i(k){}

...

};

object member

class C {

  D d;

public:

  C():d(1,2){}

...

};

constructor of super class

class C : public B {

public:

  C(int i):B(i){}

...

};

             각 형태의 초기화

   함수형 형변환(functional casting)

  정수형(int)의 변수 i를 문자형(char)의 변수 c로 변환하기 위한 문장은

    int i=65;

    char c;

    c=(char)i;

와 같다. 위의 형 변환 문장은 함수호출처럼 사용할 수 있다.

c=char(i);

  위의 문장이 비록 파라미터 i를 가지는 char()함수를 호출하는 것처럼 보이
지만, 위의 문장은 c=(char)i;와 같다. 즉, i의 형을 명시적으로 char로 변환하
여 결과를 c에 대입한다. 아래의 프로그램 조각은 A를 출력한다.

#include <iostream.h>

void main() {

    int i=65;

    char c;

    c=char(i);

    cout << c << endl;

}//main

결과는

A

이다.

  연습문제

1.


36. C++의 구조체(structure)

36. C++의 구조체
(structure)

  배열과는 달리 구조체는 다른 형(type)의 변수 여러 개를 필드(field)로 선언
하여, 한 개의 대표이름을 통하여 참조할 수 있다. 이러한 유연성 때문에 구조
체는 여전히 많이 이용되고 있으며, 어떠한 문제 - 링크드 리스트 - 는 구조
체가 아니면 설계가 불가능하기도 하다1).

  1) 물론 C++에서는 클래스로 링크드 리스트를 설계할 수 있다.

  먼저 구조체는 어떻게 선언하는가? 특이한 점은 구조체는 변수의 선언
(variable declaration)뿐만 아니라, 형의 정의(type definition)도 할 수 있
다는 것이다. 이러한 특성 때문에 구조체의 문법은 조금 복잡하다.

  만약 독자들이 회원 관리 프로그램을 만들기 위해, 회원의 이름과 나이가
필요하다고 하자. 이름은 영문자 20자를 넘지 않는다고 가정하고, 회원의 수가
100명이 넘지 않는다는 가정을 하자. 그러면 구조체를 모르는 독자들은, 다음
과 같은 배열을 선언하여 문제를 풀이하려 할 것이다.

    int age[100];

    char name[100][20];

  필요한 변수가 더 많아진다면, 이러한 변수의 선언이 더 많아질 것이고, 많
아진 변수로 인해 굉장히 골머리를 알아야 할 것이다. 하지만 그쯤은 감수할
수 있다고 하자. 진정한 문제는, 회원의 수를 예측하기 어렵다면, 고정된 길이
100의 배열을 선언해서 사용하지 못한다는 것이다. 그렇다면, 크기가 1000인
배열을 선언할 것인가? 여전히 낭비되는 메모리가 문제가 된다. 또한 1001명
째 회원이 들어오지 않는다는 보장이 없다.

  유일한 해결책은 바로 구조체를 이용하는 것이다. 서로 다른 형 - int와
char [] - 을 레코드2)의 필드로 취급하는 것이다. int와 char [20]을 필드로

  2) Pascal같은 다른 언어에서는 구조체를 레코드(record)라 한다.

가지는 구조체의 형은 다음과 같이 표현한다.

struct {

    int age;

    char name[20];

}3)

  3) 이 문장은 완전한 것이 아니다.

  여러분은 이것이 '구조체 형'이라는 점에 주목하기 바란다. int형의 변수를
선언하기 위해서는 int i; 라는 식으로 선언을 한다4). 그러므로 위의 구조체

  4) <형 이름> <변수 이름> 의 문법을 가진다는 의미이다.

형의 변수 i를 선언하기 위해서는

struct { int age; char name[20]; } i;

①                              ②

처럼 선언한다. ①은 형이름에 해당하며, ②는 변수이름에 해당한다. 컴파일러
는 흰 문자(white space)를 무시하므로, 보기 좋게 표현하면,

    struct {

        int age;

        char name[20];

    } i;

처럼 선언한다. 이와 같이 선언된 구조체 i에 대해 다음과 같은 질문을 할 수
있다.

  (1) i는 뭔가?

  (2) 필드 age와 name은 어떻게 참조하는가?

  여러분은 i가 무엇인지에 의아해 할 것이다. 주의할 점은 배열과는 달리 i는
구조체의 시작주소가 아니라는 것이다5). i는 구조체 자신(structure itself)

  5) 배열의 이름은 배열의 첫 번째 항목의 시작 주소이다. 물론 상수 주소(constant

    pointer)이므로 배열의 주소는 컴파일 시간에 결정된다.

이다.

  구조체의 필드 - 앞으로 필드를 멤버(member)라고 부르기로 하자 - 를
참조하기 위해, 필드 참조 연산자(field reference operator; . 와 ->)를 사
용한다. 그러므로 이름이 "seojt"인 30살의 회원은

    i.age=30;

    strcpy(i.name,"seojt");6)

  6) i.name="seojt"가 성립하지 않는다는 것에 주목하라. name은 상수 포인터이므로

    LValue가 될 수 없다.

라는 식으로 관리하면 된다. 아래 프로그램의 출력결과를 예측해 보기 바란다.

#include <stdio.h>

#include <string.h>

void main() {

    struct {

        int age;

        char name[20];

    } i;

    i.age=30;

    strcpy(i.name,"seojt");

    printf("%d,%s\n",i.age,i.name);

    //      30,seojt

}

  이러한 구조체가 main()함수에서뿐만 아니라, 다른 함수에서도 필요하다고
하자. 일일이 멤버를 명시해야하는 번거로움은 둘째치고, 구조체간의 대입문에
일일이 형변환(casting)을 해 주어야 할 것이다. 그러므로 구조체는 형 정의의
기능을 가지고 있다. 위에서 사용한 구조체를 SMan이라는 형으로 정의하려
면, struct와 첫번째 여는 브레이스(open brace; {)사이에 사용하려는 형 이름
을 적어 준다.

    struct SMan {

        int age;

        char name[20];

    };

  위 문장이 변수를 선언한 것이 아니라, SMan이라는 형을 정의한 것임에 유
의하자. 문장이므로 끝에 세미콜론(;)이 필요하다. C에서는 struct SMan이 형
이름이다. 그러므로 C언어에서는 SMan을 형 이름(type name)이라고 하지
않고, 꼬리표 이름(tag name)이라고 했다. 하지만 C++에서는 struct SMan도
형이고7), SMan도 형이다. 그러므로 SMan은 형 이름이다.

  7) C와의 하위 호환을 위해 이러한 사용을 허락하고 있다.

  이와 같은 형 정의를 이용해서, 위의 프로그램을 새로 작성해 보면,

#include <stdio.h>

#include <string.h>

struct SMan {

    int age;

    char name[20];

};

void main() {

    SMan i;//C에서는 struct SMan i; 라고만 해야 한다.

    i.age=30;

    strcpy(i.name,"seojt");

    printf("%d,%s\n",i.age,i.name);

    //      30,seojt

}

처럼 된다. 이제 여러분은 필요한 어느 함수에서든 SMan을 사용할 수 있다.
형 정의와 변수 선언이 동시에 가능하다는 것을 주의하자. 그러므로 다음 문
장은 Sman이라는 형을 정의하면, SMan형의 구조체 변수 i,j를 선언하고 있
다.

struct SMan {

    int age;

    char name[20];

} i,j;

  SMan의 포인터 변수가 선언된 경우, 멤머를 참조하기 위해 멤버 참조 연산
자 . 를 사용할 수 없다. SMan의 포인터 변수는 포인터이지 구조체가 아니가
때문이다. 그렇다면 아래와 같이 선언된 구조체 포인터 변수를 통해 멤버는
어떻게 접근할 것인가?

SMan *i;

  i는 포인터이지만, i가 가리키는 내용, *i는 구조체이다. 그러므로,

    (*i).age=30;

    strcpy((*i).name,"seojt");

같은 문장이 가능하다. 그렇다면, 아래의 소스는 바른 것인가?

#include <stdio.h>

#include <string.h>

struct SMan {

    int age;

    char name[20];

};

void main() {

    SMan *i;

    (*i).age=30;

    strcpy((*i).name,"seojt");

    printf("%d,%s\n",(*i).age,(*i).name);

    //      30,seojt

}

  위의 소스는 논리적인(semantic) 에러를 범하고 있다. 할당된 변수는 i이지,
*i가 아님에 주의하자. 그러므로, 우리가 마음놓고 변경할 수 있는 값은 i이지
*i가 아니다. 그럼에도 불구하고, 위의 소스에서는 *i를 건드리고 있다. 아래의
그림이 위 프로그램의 논리적인 에러를 설명하는데 도움이 될 것이다.

  그림 36-1 할당하지 않은 구조체를 건드림: i에 우연히 1000이 들어있었다고 하자. 위

    프로그램은 할당되지 않은 1000∼1021번지 사이에 값을 넣고 있다.

  여러분은 다음 규칙을 기억하고 있는가?

"변수는 사용하기 전에 항상 선언해야 한다."

  이 규칙의 의미는 할당된 메모리 공간을 사용하여야 한다는 의미이다. 포인
터 변수는 포인터 자체는 할당되었지만, 포인터가 가리키는 대상체는 할당되
지 않았으므로, 실행 시간(run-time)에 할당해 주어야 한다. 이같이 실행 시간
에 메모리를 할당하는 것을 동적 메모리 할당(dynamic memory
allocation)이라고 한다8). 여러분이 C를 사용할 때는 다음과 같이 표준함수

  8) 동적 메모리 할당에 의해서 사용되는 메모리 영역을 힙(heap)이라고 한다.

를 이용해서 메모리 할당을 했다.

i=(SMan*)malloc(22);

  형변환 - (SMan*) - 이 반드시 필요함에 유의하자. 포인터의 가감연산을
위해, 이러한 형변환은 반드시 필요하다. 구조체의 크기 계산이 복잡한 경우는
다음과 같이 문장을 작성할 수도 있으며, 이것이 일반적이다.

i=(SMan*)malloc(sizeof SMan);

  malloc()은 함수이므로, 선언되어야 한다. 그러므로 다음 문장이 소스의 첫
부분에 필요하다.

#include <mem.h>

  동적으로 할당한 메모리는 자동으로 해제(free)되지 않기 때문에, 할당한 사
용자가 반드시 해제해 주어야 한다. 이것을 메모리 해제(free)라고 한다. 이
것은 다음 함수를 사용하여 구현한다.

free(i);

  개선된 완전한 소스는 다음과 같다.

#include <stdio.h>

#include <string.h>

#include <mem.h>

struct SMan {

    int age;

    char name[20];

};

void main() {

    SMan *i;

    i=(SMan*)malloc(sizeof SMan);

    (*i).age=30;

    strcpy((*i).name,"seojt");

    printf("%d,%s\n",(*i).age,(*i).name);

    //      30,seojt

    free(i);

}

  여러분은 C++에서 동적으로 메모리를 할당/해제하는 새로운 연산자
new/delete를 알고 있을 것이다. 이것은 연산자이므로 함수의 선언이 필요없
다. 또한 매우 유동적이며, 강력하다. new/delete를 사용하여 새로 고친 소스
는 아래와 같다.

#include <stdio.h>

#include <string.h>

//#include <mem.h> 이 문장은 더 이상 필요없다.

struct SMan {

    int age;

    char name[20];

};

void main() {

    SMan *i;

    i=new SMan;

    (*i).age=30;

    strcpy((*i).name,"seojt");

    printf("%d,%s\n",(*i).age,(*i).name);

    //      30,seojt

    delete i;

}

  구조체는 구조체 포인터로 사용할 일이 훨씬 많다. 그래서 구조체의 멤버를
쉽게 참조하기 위해서 두 번째의 구조체 멤버 참조 연산자인 화살표 연산자
(arrow operator; ->)의 사용이 가능하다. (*i).age는 i->age로 사용할 수 있
다. 이 연산자의 새로운 기능은 없다. 단지, 편리한 입력을 위해서 사용될 뿐
이다. 그러므로 위의 소스는 아래와 같이 수정가능하며, 이것이 일반적이다.

#include <stdio.h>

#include <string.h>

struct SMan {

    int age;

    char name[20];

};

void main() {

    SMan *i;

    i=new SMan;

    i->age=30;

    strcpy(i->name,"seojt");

    printf("%d,%s\n",i->age,i->name);

    //      30,seojt

    delete i;

}

  그렇다면 C++에서 향상된 구조체의 기능은 꼬리표 이름(tag name)이 형 이
름(type name)으로 쓰인다는 사실 뿐인가? 그렇지 않다.

  여기에 클래스(class)의 핵심이 있다. 놀랍게도 C++의 구조체는 멤버 변수
(member variable)뿐만 아니라, 멤버 함수(member function)도 멤버로 가
질 수 있다. 그렇다면, 멤버 함수를 멤버로 가질 수 있다는 것은 무슨 의미인
가?

  좀 더 자연스러운 구조체

  위의 구조체를 일반화하기 위하여, 구조체의 멤버 변수를 조작하는 함수 등
구조체와 관계된 함수를 3개 만들기로 했다. 이들 함수는 나이를 정하고, 이름
을 정하고, 나이와 이름을 출력하는 일 등을 할 것이다.

#include <stdio.h>

#include <string.h>

struct SMan {

    int age;

    char name[20];

};

void SetAge(SMan s,int a) {

    s.age=a;

}

void SetName(SMan s,char n[]) {

    strcpy(s.name,n);

}

void Print(SMan s) {

    printf("age=%d\n"

           "name=%s\n",

           s.age,s.name);

}

void main() {

    SMan i;

    SetAge(i,30);

    SetName(i,"seojt");

    Print(i);

}

  위 프로그램의 출력결과는 얼마인가? 위 프로그램은 논리적인 에러를 범하
고 있다. i는 구조체 자신이므로, 함수의 파라미터로 전달 될 때, 값이 복사되
어 전달된다9). 그러므로 원하는 결과가 인쇄되지 않는다. 일반적으로 구조

  9) 값에 의한 전달(call by value)이라고 한다.

체는 주소를 받아야 한다. C++에서는 참조(reference)가 좋은 해결책이 될 수
도 있다.

  그럼에도 불구하고 여전히 문제점이 존재한다. 이 문제점은 문법적인
(syntax), 논리적인(semantic) 에러도 아니다. 단지 문제 그 자체와 관련된 좀
더 상위 레벨의 설계 개념의 위반이다.

  (1) 세 개의 함수는 구조체를 조작하기 위해, 매번 구조체의 주소를 받아야
한다.

  (2) 세 개의 함수는 구조체 SMan과 상관되어 있다. 하지만, 이 세개의 함수
가 SMan과 상관되어 있다는 것을 어떻게 알 수 있는가?

  (2)번 문제는 C의 설명문(comment)으로 어느 정도 처리할 수는 있지만, 완
벽한 것은 아니다. 언어 자체의 문법에 의해, 그러한 설계의 개념이 표현될 수
있으면 구조체가 강력해 질 것이다. 하여튼 논리적인 에러가 수정되고, 어느
정도 주석이 추가된 소스는 아래와 같다.

#include <stdio.h>

#include <string.h>

/*---------------- start of struct SMan -------------------*/

struct SMan {

    int age;

    char name[20];

};

void SetAge(SMan &s,int a) {

    s.age=a;

}

void SetName(SMan &s,char n[]) {

    strcpy(s.name,n);

}

void Print(SMan &s) {

    printf("age=%d\n"

           "name=%s\n",

           s.age,s.name);

}

/*---------------- end of struct SMan -------------------*/

void main() {

    SMan i;

    SetAge(i,30);

    SetName(i,"seojt");

    Print(i);

}

  문제의 핵심은 여기에 있다.

"왜 구조체와 구조체를 조작하는 함수를 별개로 구현해야 하는가?"

  우리가 존재하는 실제 세계의 대상물(object)10) - 객체 - 을 생각해 보

  10) C++에서는 객체(object)라고 한다. 객체는 실세계의 대상물을 의미하며, 자연 세계

    가 그러하듯이 객체는 정적인(static) 특성과 동적인(dynamic) 특성을 가진다. 클래스는

    이러한 객체를 모델링하는 강력한 도구(tool)이다. 정적인 특성은 멤버 변수로, 동적인

    특성은 멤버 함수로 모델링한다. 즉 C++의 문법에 의해 설계 개념이 지원된다.

자. 이러한 객체는 구조체로 표현이 불가능하다. 예를 들어 자동차라는 객체를
모의 실험하는 프로그램을 코딩한다고 가정하자. 자동차를 모델링하기 위해,
자동차의 정적인 특성 - 색, 메이커, 가격 등 - 만을 구조체로 모델링할 수
있다. 하지만, 이 구조체와 관련된 자동차의 동적인 특성 - 운전한다, 가속기
(accelerator)를 밟는다, 화면에 그린다, 등 - 은 구조체를 함수의 파라미터로
받는 함수로 구현하여야 한다. 왜 구조체와 함수를 별개로 구현하여야 하는
가?

  실세계의 객체는 항상 가시적인 것이 아니다. 또한 모든 객체가 동적인 특
성을 가지는 것도 아니다. 하지만, 구조체는 여전히 힘(power)이 약하다. 위의
회원 관리 예제에서 '회원 1명'이라는 객체는 정적인 특성을 나이(age),이름
(name)으로 모델링하였고, 동적인 특성은 나이를 정한다, 이름을 정한다, 나이
와 이름을 출력한다, 등으로 구현한 것이다. 이러한 연관된 개념은 C++ 언어
에 의해 문법적으로 지원되는 것이 바람직하다. 그렇다면, 구조체에 함수를 포
함시켜 멤버 함수를 지원하도록 하면 된다. 여러분이 주의 해야 할 것은 구조
체의 멤버 함수는 C에서 말하는 일반 함수와는 다르다. 그러므로, 정의
(definition)등에 있어 문법이 달라야 할 것이다. 걱정할 것은 없다. C++의 구
조체는 이것을 지원한다!

struct SMan {

    int age;

    char name[20];

    void SetAge(int a);

    void SetName(char n[]);

    void Print();

};

  SetAge()등의 함수의 첫번째 파리미터로 구조체의 참조가 사라진 것에 주
의하자. 이 멤버 함수들은 구조체 자신의 멤버 변수를 건드리므로 더 이상 구
조체를 파라미터로 받을 필요는 없다11). 아직은 멤버 함수의 선언이 있을

  11) 그렇다면 구조체를 위해서 메모리는 몇 바이트가 할당 되는가? 이 문제는 일단 접

    어두기로 하자. 하지만, 함수 전체의 코드가 구조체마다 할당되지 않는다는 것을 염두

    에 두기로 하자. SMan의 경우 최소한 22바이트 - age 2 바이트, name 20 바이트 -

    이상이 할당 될 것이다.

뿐이다. 이 멤버 함수들을 정의하여야 한다. 그래서 아래와 같이 정의를 추가
하였다.

#include <stdio.h>

#include <string.h>

struct SMan {

    int age;

    char name[20];

    void SetAge(int a);

    void SetName(char n[]);

    void Print();

};

void SetAge(int a) {

    age=a;//age는 도대체 어디에 할당된 것인가?

}

void SetName(char n[]) {

    strcpy(name,n);

}

void Print() {

    printf("age=%d\n"

           "name=%s\n",

           age,name);

}

void main() {

    SMan i;

    i.SetAge(30);

    i.SetName("seojt");

    i.Print();

}

  멤버 변수를 접근하는 것처럼, 멤버 함수에 대해서도 멤버 참조 연산자(. 혹
은 ->)를 사용해야 한다. 멤버 함수 SetAge()에서 age가 main()에서 선언한
구조체 i의 age라는 것을 어떻게 알 수 있는가? 핵심은 i.SetAge()에 있다.
SetAge는 i의 멤버 함수로 호출되었으므로, age와 name의 주소를 알 수 있
다. 또한 멤버 함수이므로 마지막에 함수 호출 연산자(function call
operator; () )이 반드시 사용되어야 한다.

  그럼에도 불구하고 위 프로그램에는 여전히 문법적인 에러가 존재한다. 기
억나는가? SetAge()는 멤버 함수이지 함수가 아니다. 다음의 규칙을 기억하
라.

"멤버 함수는 함수가 아니다."

  다음의 영어 문장이 여러분의 기억에 도움이 될지 모른다.

"Member function isn't function"

  그렇다면 SetAge()등이 SMan의 멤버 함수라는 것을 문법적으로 어떻게 표
현해 주어야 하는가? C++에 새로 도입된 연산자인 범위 해결사(scope
resolver; :: )를 사용하여야 한다. 고쳐진 소스는 아래와 같다.

#include <stdio.h>

#include <string.h>

struct SMan {

    int age;

    char name[20];

    void SetAge(int a);

    void SetName(char n[]);

    void Print();

};

void SMan::SetAge(int a) {

    age=a;//age는 도대체 어디에 할당된 것인가?

}

void SMan::SetName(char n[]) {

    strcpy(name,n);

}

void SMan::Print() {

    printf("age=%d\n"

           "name=%s\n",

           age,name);

}

void main() {

    SMan i;

    i.SetAge(30);

    i.SetName("seojt");

    i.Print();

}

  SMan 구조체의 멤버 함수 선언부분에는 SMan::이 없어도 되는가? 범위에
의해 컴파일러가 검사할 수 있으므로 없어도 된다. 하지만 없는 것이 아니라,
자동으로 붙여지는 것이다. 그러므로 명시적으로 SMan::을 적어주어도 아무
상관이 없다. 하지만 일반적으로 그렇게 하지는 않는다.

struct SMan {

    int age;

    char name[20];

    void SMan::SetAge(int a);

    void SMan::SetName(char n[]);

    void SMan::Print();

};

  이제 여러분은 C++의 변경된 구조체를 사용하여 실세계의 객체를 모델링한
것이다! 클래스는 이처럼 멤버 함수를 멤버로 가질 수 있는 구조체 형을 의미
한다. 다시 말하면, 실세계의 객체의 정적인 특성과 동적인 특성을 만들어 내
는 틀(template)이 되는 것이다. 그렇다면 struct 대신에 class를 사용해도 된
다는 말인가? 그렇다. 단, class에서 사용하는 몇 가지 규칙 때문에, 몇 가지
변경이 필요하다. 완성된 완전한 소스는 아래와 같다.

#include <stdio.h>

#include <string.h>

class SMan {

  private://이 후의 멤버들의 접근 권한이 private임을 선언한다.

    int age;

    char name[20];

  public://이 후의 멤버들의 접근 권한이 public임을 선언한다.

    void SetAge(int a);

    void SetName(char n[]);

    void Print();

};

void SMan::SetAge(int a) {

    age=a;

}

void SMan::SetName(char n[]) {

    strcpy(name,n);

}

void SMan::Print() {

    printf("age=%d\n"

           "name=%s\n",

           age,name);

}

void main() {

    SMan i;

    i.SetAge(30);

    i.SetName("seojt");

    i.Print();

}

  SMan형의 변수 i를 선언했을 때, SMan을 형(type), i를 변수(variable)라
고 하는 것은 어색하다. i는 더 이상 변수가 아니다12). 그러므로 클래스에

  12) 변수가 어떻게 함수를 가질 수 있는가?

대해서, SMan을 클래스(class), i를 객체(object)라고 한다. 그리고 이러한
것을 '변수를 선언한다'라고 하지 않고, '클래스 SMan의 객체 i를 만든다'고
한다. 또한 객체를 만드는 과정을 '예화(instantiation)한다'고 한다. 예화된
객체를 예(instance)13)라고 한다. 그렇다면 인스턴스와 객체는 같은 것인

  13) 혼동을 피하기 위해 본 책에서는 예(instance)를 인스턴스라고 부른다.

가? 그렇다. 하지만, 관점의 차이는 있다. 클래스 SMan의 입장에서 i는 자신
의 인스턴스(instance)이다. 하지만, 객체 i의 입장에서 자신은 SMan의 객체
(object)이다.

  클래스가 선언되었을 때, 객체를 만드는 것은 무척 간단하며, 실세계의 개념
과 일치한다는데 주목하라. 회원이 1명 증가할 때마다, 독자들이 해야 할 일은
SMan의 객체를 하나 더 만드는 일뿐이다. 함수를 수정하거나, 함수를 추가해
야 하는 일은 이제 더 이상 발생하지 않는다.

...

void main() {

    SMan i;

    i.SetAge(30);

    i.SetName("seojt");

    i.Print();

    SMan j;

    j.SetAge(0);

    j.SetName("baby");

    j.Print();

}

  연습문제

1.


37. C++ 객체 모델(object model)

37. C++ 객체 모델
(object model) 그리고
오토마타(automata)!

  객체 지향 프로그래밍에서 사용하는 객체 모델(object model)을 이용하여
문제를 어떻게 디자인(design)해야 하는가? 객체의 특성을 C++로 어떻게 나
타내는 것이 좋은가? 객체 모델이 모든 문제를 해결하는 왕도(kingly way)인
가? 이 장에서는 (필자의 경험과 텍스트의 이론을 바탕으로) 객체 모델링을
설명한다.

  클래스, 객체 그리고 인스턴스(class, object
and instance)

  먼저 실세계(real world)에서 객체(object)에 대해서 살펴보자. 객체란 우리
가 보고, 만지고, 느끼는 실세계에 이름이 붙여진 모든 대상이다.

  예를 들면, ①자동차, 컴퓨터 같은 사람이 만든 작은 물건, ②아파트, 집 같
은 큰 물건,  ③나무, 꽃, 동물 같은 하나님이 만든 생명체, ④공기, 우주 같은
만질 수 없는 대상, ⑤사랑, 느낌 같은 감정들이 모두 객체이다.

  객체 모델이란 문제를 해결하기 위한 형식적인 모델(formal model)로 객체
에 중점을 두는 모델을 말한다. 즉, 문제의 해결 과정 및 내부 상태를 객체의
상호작용(object interaction)으로 기술하려 시도한다. [필자는 기회가 되었을
때 클래스(class)를 설명하기 위해 클래스에 대한 설명을 뒤로 미룰 것이다.
하지만, 클래스를 정의하기 전에 클래스란 용어를 사용할 것인데, 일단은 클래
스를

'객체를 기술하는 틀(template)'

로 알아두자. 이 표현은 실제로 정확한 것이다! 예를 들면 '사람이라는 객체'
는 사람을 기술하는 틀인 '사람 클래스'에 의해 실세계에 만들어진다.]

  하지만, 객체 모델로 풀이하는 것이 오히려 복잡한 본질적인 문제가 있을
수 있으므로 그러한 문제는 (오토마타 같은) 다른 모델로 풀이하는 것이 바람
직하다.

  아래의 그림을 객체로 모델링하는 것에 대해 생각해보자.

             뽀리가 배나무를 쳐다보는 장면의 모델링: 뽀리, 배나무라는 2개의 객체
          가 필요하다고 생각하는 것은 객체 모델링을 잘못 이해한 것이다.

  그림에는 사람과 나무가 있다. 사람을 CMan⼆, 나무를 CTree라는 객체로

             클래스(class)의 첫 문자로 C를 붙이는 관례(convention)를 사용한다.

모델링하기로 하자. 하지만, 좀 더 주의 깊게 살펴보면, 배경(CBackground)
및 생각(CThinking)이라는 객체가 필요한 것을 알 수 있다.

  물론 배경은 사람과 나무와는 독립적으로 존재하는 것이지만, 생각은 사람
에 종속적인 객체이다. 즉, 사람만이 생각할 수 있다. 사람의 이름(name)이
'신뽀리'라면 이것은 사람 객체의 정적인 속성(static attribute)이 될 것이다.
사람이 생각한다(Thinking), 쳐다본다(Watching)는 것은 동적인 속성
(dynamic attribute)이 될 것이다. 객체 모델링에서 정적인 속성은 멤버 변
수(data member, member variable)로, 동적인 특성은 멤버 함수(member
function)⼆로 구현한다.

             C++의 표준 용어는 데이터 멤버와 멤버 함수이다. 다른 언어에서는, 데
          이터 멤버를 프라퍼티(property), 멤버 함수를 메서드(method)로 부르기도
          하는데, 우리는 이러한 용어를 자유롭게 사용하기로 하자. 독자들이 후에
          Windows 프로그램을 배울 때, 객체의 특별한 종류인 OLE 객체에 대해서
          배울 것인데, OLE 객체는 프라퍼티와 메서드이외에 이벤트 핸들러(event
          handler)라는 특별한 멤버 함수를 가진다.

             객체의 모델링: 3줄의 네모에 차례대로 클래스 이름(class name), 데이터
          멤버와 멤버 함수를 명시한다. 이것은 표준 UML(unified modelling
          language) 표기법이다. CMan의 데이터 멤버는 이름을 의미하는 name이 있
          고, 멤버 함수는 생각하다와 쳐다보다를 의미하는 Thinking()과 Watching()이
          있다. CTree의 데이터 멤버는 나무 몸통의 색을 의미하는 color와 나무의
          크기를 의미하는 height가 있다. 멤버 함수는 나무가 자라는 것을 의미하는
          Grow()가 있다.

  CMan와 CTree를 다음과 같이 구현할 수 있을 것이다. 소스에서 사용한
public: 에 대해서는 일단 무시하자(이것은 접근 지정자(access qualifier)인
데, private, public 혹은 protected를 명시할 수 있다). 소스에 클래스 이름과
같은 함수가 존재하는데 이것은 생성자(constructor)라고 하는 특별한 종류
의 멤버 함수이다. 이것은 이 장의 '인스턴스(instance)' 절(section)에서 설명
할 것이고, 자세한 설명은 '생성자,소멸자'를 참고하라.

enum COLOR { BROWN=0, BLACK=1, RED=2 };

////////////////////////////////////////////////////////////////

// CMan

class CMan {

public://멤버들의 접근 여부를 지정한다. public 멤버는 만들어진 객체에서

       //마음대로 읽고 쓸 수 있는 멤버를 지정한다.

    //static attributes

    char name[80];

    //dynamic attributes

    CMan(char* sName) { strcpy(name,sName); }//생성자(constructor)

    void Thinking() { printf("%s is thinking...\n", name); }

    void Watching() { printf("%s is watching...\n", name); }

};//class CMan

////////////////////////////////////////////////////////////////

// CTree

class CTree {

public:

    //static attributes

    int color,height;

    //dynamic attributes

    CTree(int c,int h) { color=c; height=h; }

    void Grow() { printf("Tree is growing...\n"); }

};//class CTree

  나무라는 객체는 조금 추상적(abstract)이다. 이 나무의 종류가 '사과나무
(CApple)'라면 '사과나무'라는 객체는 '나무'라는 객체의 모든 속성을 공유하면
서, 자신만의 속성을 가질 것이다. 이것은, 대부분의 학자들에 의해서 거짓이
라고 인정된 진화론에 따르면, 진화 트리(evolutionary tree)의 CApple의 조상
의 위치에 CTree가 위치하는 것이다.

             사과나무와 나무의 관계: CTree의 대부분의 특성을 공유하는 사과나무
          (CApple), 배나무 등이 있을 수 있다. 즉, 나무는 트리 구조에서 사과나무의
          상위(base)에 위치한다. 사과나무는 나무지만, 나무는 사과나무가 아니라는
          사실에 주의하자.

  사과 나무는 아래 그림처러 모델링할 수 있다.

             사과나무의 모델링: CTree의 대부분의 특성을 공유하면서, 사과나무 자신
          만의 특징을 추가로 가질 것이다. apple, leaf는 사과나무의 사과의 개수와
          잎의 개수를 의미한다. Ripe()는 사과의 익은 정도를 의미하는 멤버 함수이
          다. 추가한 멤버는 굵게 표시하였다.

  하지만, 진화 트리에서 사과나무는 다음과 같이 나타낸다. 화살표의 방향에
대해서 주의하라.

             CApple과 CTree의 관계 표현: 자세한 것에서 일반적인 것으로 속이 빈
          화살표(hollowed arrow)를 그려 클래스의 관계를 표시한다. 이것은 CTree가
          CApple의 베이스에 위치하는 것을 나타낸다. 이러한 클래스 관계의 이름을
          IS-A 관계(is-a relationship)라 한다. 이것은 'CApple is a CTree'라는 문장을
          자연스럽게 반영한다. 이것은 또한 화살표의 방향을 결정하는 이유이다.

  위 그림처럼 클래스가 표시되었을 때, CApple은 CTree의 자손(decendent)
으로 해석해야 한다. CTree는 CApple의 부모(parent)가 된다. 자식은 부모의
(공개된) 모든 특성을 공유한다. 즉, 위의 그림에서 CApple의 멤버함수에는
CTree의 멤버 함수인 Grow()가 포함된다. 그러므로, CApple의 객체에서(이것
이 apple이라면) apple.Grow()를 호출하는 것은 타당하다.

IS-A 관계(is-a relationship)

  CTree와 CApple의 관계는 어떤 관계인가? 진화 트리에서 이들 클래스는
부모와 자식관계(parent and child relationship)이다. C++에서 이들간의 관계
는 IS-A 관계(is-a relationship)라 한다. 왜냐하면, 'CApple은 CTree이다
(CApple is a CTree)'라는 문장이 성립하기 때문이다. 자세한 것은 일반적인
것이다!

  IS-A 관계를 소스에 어떻게 명시해야 할까? CApple IS-A CTree의 관계는
CApple 클래스를 정의할 때, class CApple뒤에 콜론(:)을 사용하여, 베이스 클
래스(base class)를 명시한다.

class CApple : CTree { ...

  IS-A 관계를 명시할 때, 베이스 클래스의 멤버들에 대한 접근 권한을 변경
할 수 있는데, 이것을 위한 접근 변경자(access modifier)⼆ private, public

             접근 변경자(modifier)와 접근 지정자(qualifier)는 구분되어야 한다. 이들은
          비록 같은 3개의 키워드 private, public 과 protected를 사용하지만, 사용한
          위치에 따라, 변경자로 혹은 지정자로 불린다.

과 protected를 명시할 수 있다. 일단은 public 만을 사용하기로 하자.

class CApple : public CTree { ...

  또한 베이스 클래스의 생성자를 유도 클래스의 생성자에서 호출해 주어야
할 것이다. 그것은

CApple(int c,int h) : CTree(c,h) { apple=leaf=0; }

에서처럼 유도 클래스의 생성자를 정의할 때, 콜론 뒤에 베이스 클래스의 생
성자를 명시적으로 호출하면 된다. 자세한 사항은 '생성자,소멸자'에서 설명할
것이다.

  이제 CApple 클래스를 구현해 보자. 소스는 아래와 같다.

class CApple : public CTree {

public:

    //static attributes

    int apple,leaf;

    //dynamic attributes

    CApple(int c,int h) : CTree(c,h) { apple=leaf=0; }

    void Ripe() { printf("%d apple is ripe\n",apple); }

};//class CApple

  위의 소스를 보면 CApple의 멤버 함수가 Ripe()뿐인 것 같지만, CApple의
베이스 클래스로 CTree가 명시된 것을 주의하라. 그러므로, CTree의 멤버 함
수인 Grow()도 CApple의 멤버 함수이다. 그러므로, CApple에서 Grow()를 호
출할 수 있다.

상속(inheritance)

  이처럼 클래스 간의 IS-A 관계를 상속(inheritance) 관계라 한다. 위의 예
에서 CApple은 CTree를 상속받고 있다. 상속 받는 클래스를 서브 클래스
(sub class), 상속을 주는 클래스를 수퍼 클래스(super class)라하며, 서브
클래스를 유도 클래스(derived class), 수퍼 클래스를 기저 클래스(base
class)라고도 한다. 어느 용어를 사용해도 상관은 없지만, 서브-수퍼, 유도-기
저의 짝은 맞추어서 사용해야 한다. 예를 들면 다음 문장은 조잡(coarse)하다.

'CApple은 베이스 클래스 CTree로 부터 상속된 서브 클래스이다.'

  베이스에는 디라이브드(derived)를 수퍼에는 서브를 항상 사용하라!

  유도 클래스가 오직 1개의 베이스 클래스를 가질 때, 이것을 단일 상속
(single inheritance)라 한다. 2개 이상의 베이스 클래스를 가지는 예는 후에
살펴보겠지만, 이것을 다중 상속(multiple inheritance)이라 한다.

  나무가 사과나무에 비해 더 일반적이라는 사실을 주의하자. 일반적일수록
많은 것을 포함하지만, 속성은 적어진다. 예를 들면, '자동차'는 '프라이드'에
비해 일반적이다. '자동차'라고 하면, 이 세상의 모든 자동차를 포함할 수도 있
지만, '프라이드 자동차'라고 하면, 그 많은 자동차의 일부를 나타낸다.

  그러므로, '자동차'를 기술하는데 2줄의 문장이 필요하다면, '프라이드'를 기
술한는데는 자동차를 기술하는데 사용한 2줄의 문장 외에, 10줄 정도의 문장
이 더 필요할 것이다. 자 객체 모델링에서 다음의 규칙을 외워두자.

'객체가 일반적일수록 표현해야할 속성은 적다.'

  나무와 사과나무의 관계에 대해서, 다시 생각해 보자. 사과나무는 더 구체적
인 것이므로, 나무에 비해 기술해야 할 것이 많다. 또한, 사과나무는 나무지만,
나무는 사과나무가 아니다(Apple tree is a tree, but Tree is not a apple
tree). 이것은 객체간의 관계에서 중요한 IS-A 관계를 설명한다. 객체간의 관
계를 영어 문장으로 표현할 때, 'is a'의 주어에는 자세한 것이, 'is a' 의 보어
에는 일반적인 것이 온다. 이것을 객체간의 관계에서

주어→보어

로 나타내기로 하자. 이것은 영어 문장

주어 is a 보어

를 의미하는 것으로, 객체로 향하는 화살표의 방향을 결정한다. 즉,

CApple IS-A CTree

의 영어 문장이 성립한다. 이것을

'CApple과 CTree에는 IS-A 관계(is-a relationship)가 있다.'

고 말한다(클래스의 순서에 주의하라).

   인스턴스(instance)와 객체: 또 다른 나

  이제 객체를 기술하는 작업이 어느 정도 완성되었다. 지금까지 완성된 클래
스 계층(class hierarchy) 관계는 다음과 같이 표현할 수 있다.

             클래스 계층도(class hierarchy diagram): 프로그램의 전체적인 구조를 이런
          그림으로 모두 표현할 수 있다는 것은 얼마나 강력한가! CTree는 CApple
          의 베이스 클래스이다. 이와는 별도로 CMan 클래스가 존재한다. 클래스
          계층도는 클래스 간의 상호 작용은 완벽하게 기술하지 못한다. 후에 클래
          스 간의 상호 작용을 기술하는 모델로 오토마타를 제시할 것이다.

  위의 클래스 계층도(class hierarchy diagram)가 프로그램에 사용한 모든
클래스의 관계를 나타낼 수 있다는 것은 놀랄만한 일이다! 하지만, 이 사실을
상기하도록 하자.

'클래스 계층도는 클래스 간의 상호작용(interaction)을 기술하지 못한다.'

  예를 들면, 위의 그림은 CMan과 CApple이 어떻게 서로 상호 동작해야 하
는지 전혀 기술하지 않고 있다. 물론, 표준 UML에서는 이러한 상호작용을 기
술하는 체계적인 방법을 제공하지만, 필자는 상호작용의 기술을 위해 더 강력
한 모델인 오토마타(automata)를 이 장의 후반부에서 제시할 것이다.

  CMan 클래스에 집중해 보자. 이 클래스를 아래에 다시 리스트하였다.

class CMan {

public:

    char name[80];

    CMan(char* sName) { strcpy(name,sName); }//constructor

    void Thinking() { printf("%s is thinking...\n", name); }

    void Watching() { printf("%s is watching...\n", name); }

};//class CMan

  필자는 지금까지 클래스와 객체를 혼합하여 사용하였다. 이제 클래스와 객
체를 엄격하게 구분할 좋은 때이다. 위의 소스는 클래스를 정의한 것이지, 객
체를 정의한 것이 아니다. 객체는 본질적으로 C++로 정의할 수 없다. 다만 만
들(create) 수 있을 뿐이다.

  객체를 만들기 위해서는 '그 객체'를 어떻게 만들어야 하는가? 에 대한 설명
서가 필요하다. 이 설명서가 '클래스'이다. 다시 한번 클래스와 객체를 정의하
면,

'객체를 만들기 위한 설명서가 클래스이고, 그 클래스를 이용하여
만들어진 물건이 객체다.'

  독자들은 이 후부터의 문장에서 '객체'와 '클래스'란 용어의 사용에 대해 주
의 깊게 읽도록 하라. 독자들도 '클래스'가 사용될 자리에 '객체'를 사용하지
않기를 바란다.

  이제 CMan이란 클래스의 객체 a를 만들기 위해서는

CMan a;

라고 적는다. 그러면, 클래스 CMan을 기술한 모든 정보를 이용하여, 객체 a가
실제로 메모리에 할당된다. a를 구성하는 데이터 멤버와 멤버 함수가 실제로
어떻게 메모리에 할당되는지는 무시하라! 이것은 후에 자세하게 설명할 것이
다. 일단은 a가 메모리에 할당된다는 사실을 주목하라. 즉, class CMan...으로
시작하는 문장은 객체를 만들기 위한 코드일 뿐이고, 실제 CMan에서 정의한
문장대로 메모리에 할당하는 문장은 CMan a; 인 것이다.

  이제 a는 사람(CMan)중에 어떤 사람인가? 그 사람은 구체적인 a라는 사람
인 것이다!

  객체를 만들 때, 초기화 작업등을 위해 자동으로 멤버 함수가 불리도록 멤
버 함수를 정의할 수 있다. 이 함수를 생성자(constructor)라 하는데, 객체가
생성될 때 자동으로 불리기 때문에 붙여진 이름이다. 생성자는 클래스의 이름
과 같은 함수이름을 가지며, 파라미터를 자유롭게 명시할 수 있다. 또한 함수
의 리턴형은 명시하지 않아야 한다. 예를 들면 CMan의 생성자는

CMan(char* sName) { strcpy(name,sName); }//constructor

처럼 정의⼆할 수 있다. 생성자가 파라미터를 가지는 경우, 생성자를 파라미터

             유도 클래스(derived class)의 생성자를 정의하는 경우, 베이스 클래스의
          생성자를 명시해야 하는 경우(베이스 클래스에 디폴트 생성자가 없는 경
          우)가 있다. 이 때 유도 클래스의 생성자의 헤더와 블락 사이에 세미 콜론
          을 적고 베이스 클래스의 생성자를 명시적으로 직접 호출한다. 예를 들어,
          CMan의 베이스 클래스가 CClay이고 파라미터를 sName과 2로 설정해야
          한다면(찰흙으로 만든 아담과 이브를 의미하는), CMan(char* sName) :
          CClay(sName,2) { ... 로 적는다. 자세한 설명은 '생성자,소멸자'를 참고하
          라.

를 객체의 이름 뒤에 명시해야 한다. 위의 예에서 객체의 생성자는 '사람의 이
름'인데 '신뽀리'라는 사람을 만들기 위해서는

CMan a("신뽀리");

처럼 객체를 선언한다. 이 문장은 객체 a를 메모리에 할당한 후, CMan(char*)
생성자 함수를 자동으로 호출한다. 객체 a의 데이터 멤버 name은 '신뽀리'로
설정된다. 아래의 예를 보자.

#include <stdio.h>

#include <string.h>

class CMan {

public:

    char name[80];

    CMan(char* sName) { strcpy(name,sName); }//constructor

    void Thinking() { printf("%s is thinking...\n", name); }

    void Watching() { printf("%s is watching...\n", name); }

};//class CMan

void main() {

    CMan a("신뽀리");//구체적인 메모리 할당은 설명을 뒤로 미룬다.

    CMan b("박광수");

    a.Thinking();

    b.Watching();

}//main()

  실행 결과는

    신뽀리 is thinking...

    박광수 is watching...

이다. CMan의 입장에서 a,b는 자신의 형태로 만들어진 다른 속성을 가지는
한 예(instance)들이다. 즉 사람이라는 클래스의 형태로 구체적인 객체 a와 b
가 만들어졌는데, 이것은 CMan의 구체적인 예(instance)들인 것이다. 그래서
클래스의 입장에서 객체를 만드는 것을 예화한다(instantiate)고 표현한다.
하지만, 클래스 외부에서 이러한 예화를 보았을 때, a,b는 CMan 클래스의 객
체인 것이다. 즉 예(instance)와 객체(object)는 같은 뜻에 대한 입장 차이에서
나온 다른 말이다.

  정리하면, 변수 선언

int i;

에서 int 는 형 이름(type name)이고, i는 그 형의 (메모리에 할당된) 구체적
인 형태인 변수(variable)이다. 하지만, 객체 선언

CMan a("신뽀리");

에서 CMan은 클래스 이름(class name)이고, a는 그 클래스의 (메모리에 할당
된) 구체적인 형태인 객체(object)이다.

불완전한 예

  이제 모두가 결합된 실행 가능한 소스를 아래에 리스트하였다.

#include <stdio.h>

#include <string.h>

enum COLOR { BROWN=0, BLACK=1, RED=2 };

////////////////////////////////////////////////////////////////

// CMan

class CMan {

public:

    //static attributes

    char name[80];

    //dynamic attributes

    CMan(char* sName) { strcpy(name,sName); }

    void Thinking() { printf("%s is thinking...\n", name); }

    void Watching() { printf("%s is watching...\n", name); }

};//class CMan

////////////////////////////////////////////////////////////////

// CTree

class CTree {

public:

    //static attributes

    int color,height;

    //dynamic attributes

    CTree(int c,int h) { color=c; height=h; }

    void Grow() { printf("Tree is growing...\n"); }

};//class CTree

////////////////////////////////////////////////////////////////

// CApple -> CTree

class CApple : public CTree {

public:

    //static attributes

    int apple,leaf;

    //dynamic attributes

    CApple(int c,int h) : CTree(c,h) { apple=leaf=0; }

    void Ripe() { printf("%d apple is ripe\n",apple); }

};//class CApple

void main() {

    CApple appleTree(BROWN,100);

    appleTree.Grow();

    appleTree.Grow();

    appleTree.Ripe();

}//main()

  실행 결과는

    Tree is growing...

    Tree is growing...

    0 apple is ripe

이다.

  appleTree.Ripe()로 Ripe()를 호출한 사실에 주목하라. Ripe()가 CApple에
정의되지는 않았지만, CTree에 정의하였으므로, 이렇게 사용하는 것은 불법이
아니다. 하지만, 출력 결과는 다소 불만이다. 왜 사과나무가 자라지 않고 나무
가 자라는가?

Tree is growing...

은

Apple Tree is growing...

으로 출력되는 것이 자연스러울 것이다. 또한, 2번씩이나 사과나무가 자랐다
면,

0 apple is ripe

가 출력되는 것보다, 1번 Grow()가 호출될 때마다 5개의 사과가 익어

10 apple is ripe

가 출력되는 것이 바람직해 보인다. 이것을 어떻게 구현해야 하는가? 그렇다.
Grow()함수를 CApple에 맞게 재정의(redefinition)해야 한다!

   오버라이딩(overriding)

  IS-A 관계에서 상위 클래스(super class)에 정의된 멤버는, 필요하다면, 하
위 클래스(sub class)의 동작에 맞도록 적당하게 수정되는 것이 바람직하다.
예를 들면, CTree의 Grow()의 동작은 CApple에 맞게

'Apple Tree is growing...'

을 출력하는 것이 바람직할 것이다. 이것은 Grow()란 함수를 CApple에서 재
정의하면 된다. 이러한 재정의를 오버라이딩(overriding)이라 한다. 오버라이
딩을 파라미터가 다르다면 같은 이름을 사용해도 좋은 오버로딩
(overloading)과 구분하라. 오버라이딩은 상속 관계에서 상위 클래스에서 정
의된 멤버 함수와 꼭 같은 멤버 함수를 하위 클래스에서 정의하여, 상위 클래
스의 함수를 무시(override)하는 것을 말한다. 만약 함수의 이름이 같고, 파라
미터가 다르다면, 이것은 오버라이딩이 아니라, 오버로딩인 것이다.

  CApple에서 Grow()를 오버라이드해 보자. ① 먼저 CApple 클래스에
CTree::Grow()와 같은 멤버 함수를 선언한다.

class CApple : public CTree {

    ...

    void Grow();//override CTree::Grow()

};//class CApple

  그리고 ② Grow()를 정의하자.

void CApple::Grow() {

    printf("Apple Tree is growing...\n");

    apple+=5;

    leaf+=10;

    height+=1;

}

  Grow()는 이제 'Apple tree is growing...'을 출력한다. 그리고, 사과, 잎과
높이를 각각 5, 10, 1식 증가시킨다. 아래의 예를 보자.

#include <stdio.h>

#include <string.h>

enum COLOR { BROWN=0, BLACK=1, RED=2 };

////////////////////////////////////////////////////////////////

// CTree

class CTree {

public:

    //static attributes

    int color,height;

    //dynamic attributes

    CTree(int c,int h) { color=c; height=h; }

    void Grow() { printf("Tree is growing...\n"); }

};//class CTree

////////////////////////////////////////////////////////////////

// CApple -> CTree

class CApple : public CTree {

public:

    //static attributes

    int apple,leaf;

    //dynamic attributes

    CApple(int c,int h) : CTree(c,h) { apple=leaf=0; }

    void Ripe() { printf("%d apple is ripe\n",apple); }

    void Grow();//overRIDE CTree::Grow()

    void Grow(int i);//overLOAD Grow()

};//class CApple

void CApple::Grow() {//CTree::Grow()를 오버라이딩하였다.

    printf("Apple Tree is growing...\n");

    apple+=5;

    leaf+=10;

    height+=1;

}//CApple::Grow()

void CApple::Grow(int i) {//CTree::Grow()를 오버로딩하였다.

    printf("Apple Tree is growing...\n");

    apple+=i;

}//CApple::Grow()

void main() {

    CApple appleTree(BROWN,100);

    appleTree.Grow();

      //override된 CTree::Grow()를 호출한다.

    appleTree.Grow(2);

      //overload된 CTree::Grow()를 호출한다.

    appleTree.Ripe();

}//main()

  실행 결과는

    Apple Tree is growing...

    Apple Tree is growing...

    7 apple is ripe

이다. 이제 appleTree.Grow()는 CApple::Grow()를 호출한다. 이것은 가까운
곳을 먼저 보는 범위 규칙(scope rule)에 의해서 결정된 것이다. 물론
appleTree가 CTree::Grow()를 호출할 수도 있다. 문법이 좀 까다로워 보이지
만,

appleTree.CTree::Grow();

는 CApple의 객체를 이용하여, CTree의 Grow()를 호출한다. 즉 오버라이딩되
기 전의 Grow()를 호출하는 것이다. appleTree.Grow(2)는 CApple::Grow(int
i)를 호출한다. 오버로딩된 2번째 Grow()를 호출하는 것이다. 그래서 7개의 사
과가 익는다. main()을 아래와 같이 고치면 결과는 어떻게 될까?

void main() {

    CApple appleTree(BROWN,100);

    appleTree.CTree::Grow();

    appleTree.Grow();

    appleTree.Ripe();

}//main()

  결과는

    Tree is growing...

    Apple Tree is growing...

    5 apple is ripe

이다. 결과의 이유를 독자들은 충분히 설명할 수 있을 것이다. 오버라이딩된
Grow()에서 상위 클래스의 Grow()를 호출하는 것도 같은 문법을 사용하여
가능하다. 사실 Grow()는 다음과 같이 고치는 것이 바람직하다.

void CApple::Grow() {//CTree::Grow()를 오버라이딩하였다.

    printf("Apple ");

    CTree::Grow();//상위 클래스의 Grow()를 호출한다.

      //Grow();를 사용하면 어떻게 될까?

    apple+=5;

    leaf+=10;

    height+=1;

}//CApple::Grow()

  즉 오버라이딩을 하는 경우, 베이스 클래스에서 하던 일에 어떤 일을 추가
하는 것이므로, 베이스 클래스의 원래 함수를 불러주고 추가적인 작업을 코딩
하는 것이 바람직한 것이다. 위의 소스에서 CTree::Grow()를 Grow()로 고치
면, 어떻게 될까? 독자들이 직접 고친 소스를 실행해보기 바란다. 무한 루프에
빠진 프로그램은 'Stack overflow!' 에러를 발생하여 GPF가 발생할 것이다!
(독자들은 그 이유를 설명할 수 있는가?)

   접근 권한의 지정과 변경

  클래스의 모든 멤버들을 객체가 모두 접근(access)하는 것은 바람직하지 못
한 경우가 많다. 클래스의 멤버에 대해서 객체의 접근 권한을 명시할 수 있는
데 이것은 접근 권한 지정자(access rights qualifier)를 사용한다. 접근 지
정자에는 private, public과 protected가 있다.

접근 지정자

설명

private

public

protected

객체에서 접근할 수 없다.

객체에서 접근할 수 있다.

객체에서는 접근할 수 없고, 유도 클래스에서만
접근할 수 있다.

  클래스 안에서 접근 지정자가 한 번 명시되면, 다음 접근 지정자를 만나기
전까지 모든 멤버에 영향을 미친다. 접근 지정자는 라벨(label)의 문법을 가지
므로, 끝에 콜론(:)을 적고, 콜론 뒤에는 아무 것도 적을 수 없다. 아래의 문법
은 틀렸다.

public: int i;

  아래와 같은 클래스를 고려해 보자.

class CBase {

private:

    int a;

    void F() { printf("F()\n"); }

protected:

    int b;

    void G() { printf("G()\n"); }

public:

    int c;

    void H() { printf("H()\n"); }

    CBase() { a=1; b=2; c=3; }

};//class CBase

  위의 클래스는 private, protected와 public 멤버를 각각 2개씩 가지고 있다.
a, F()는 객체에서 접근할 수 없다. b, G()는 CBase에서 유도된(derived) 클래
스에서만 접근할 수 있다. 즉, b, G()는 외부로부터 보호된다. c, H()는 객체에
서 접근할 수 있다. 아래의 예에서 에러를 주석 처리해 보라.

...

void main() {

    CBase i;

    i.a=100;

    i.F();

    i.b=200;

    i.G();

    i.c=300;

    i.H();

}

  독자들은 아래와 같이 고쳤는가?

...

void main() {

    CBase i;

    //i.a=100; // private 멤버는 객체에서 접근할 수 없다.

    //i.F();

    //i.b=200; // protected 멤버도 객체에서 접근할 수 없다.

    //i.G();

    i.c=300;

    i.H();

}

결과는

H()

이다.

  독자들이 가끔 혼동하는 사실은

private: 멤버를 객체에서 접근할 수 없다.

란 문장을

private: 멤버를 접근할 수 없다.

로 잘못 기억하기 때문에 발생한다. CBase의 멤버 H()에서 a, F(), b, G()를
접근하는 것은 불법인가?

class CBase {

private:

    int a;

    void F() { printf("F() %d\n",a); }

protected:

    int b;

    void G() { printf("G() %d\n",b); }

public:

    int c;

    void H() {

        a=100;

        F();

        b=200;

        G();

        printf("H() %d\n", c);

    }

    CBase() { a=1; b=2; c=3; }

};//class CBase

  위의 예에서 보면, H()가 private: 및 protected: 멤버를 접근하고 있다. 그러
므로, 이것은 불법이다고 생각한 사람은

객체에서

를 빠뜨린 사람이다. H()는 클래스의 멤버 함수이기 때문에, 접근 권한에 상
관없이 모든 멤버를 접근할 수 있다. 아래의 예를 보자.

#include <stdio.h>

class CBase {

private:

    int a;

    void F() { printf("F() %d\n",a); }

protected:

    int b;

    void G() { printf("G() %d\n",b); }

public:

    int c;

    void H() {

        a=100;

        F();

        b=200;

        G();

        printf("H() %d\n", c);

    }

    CBase() { a=1; b=2; c=3; }

};//class CBase

void main() {

    CBase i;

    i.c=300;

    i.H();

}

  실행 결과는

    F() 100

    G() 200

    H() 300

이다. 위의 예에서 CBase의 생성자 CBase()가 public: 권한을 가지는 사실에
주목하라. 만약 생성자가 private: 혹은 protected: 라면 CBase 객체를 만드는
것이 불가능하다. 독자들은 이유를 설명할 수 있는가?

  접근 지정자에서 사용한 같은 키워드 3개가, 상속에서 접근 권한을 변경하
기 위해 사용된다. 이 때 이들을 접근 권한 변경자(access rights modifier)
라 한다. 접근 변경자는 유도 클래스를 정의할 때, 베이스 클래스의 앞에 명시
한다. 예를 들면,

class CDerived : public CBase {...

는 CDerived 객체에서 CBase의 멤버를 접근하는 권한을 변경한다. 독자들이
주의할 사항은

'CDerived 클래스에서가 아닌, CDerived 객체에서 CBase의 멤버를
접근하는 권한을 변경한다.'

라는 것이다. 베이스 클래스에서 접근 지정자로 명시한 권한이, 유도 클래스의
접근 변경자에 의해서, 유도 객체에서의 접근 권한이 어떻게 바뀌는지 아래
표를 참고하라.

      유도 클래스

(modifier)

베이스 클래스

(qualifer)

private

protected

public

private

n/a

n/a

n/a

protected

private

protected

protected

public

private

protected

public

             권한의 변경: 베이스 클래스에서 지정자(qualifier)로 명시한 권한은 유도
          클래스에서 변경자(modifier)로 바꿀 수 있다. 이것은 베이스 클래스에 영향
          을 미치는 것이 아니라, 유도 클래스의 객체에 영향을 미친다.

  위의 표에서 n/a(Not Available)로 표시된 부분에 주목하라. 이것은 베이스
클래스에서 private:로 명시된 부분은, 유도 클래스에서 어떤 변경자를 사용하
여 상속받든지 접근할 수 없음을 의미한다.

  사실 private: 멤버는 객체에서도 유도 클래스에서도 접근할 수 없다. 개인
의 사생활(privacy)은 자식도 접근할 수 없는 것이다. 이것을 '프라이버시의
원리(privacy principle)'라 한다. 독자들은 후에 '친구(friend)'에서 이러한
private: 멤버를 접근할 수 있는 권한이 오직 친구(friend)에게만 주어진다는 '
친구의 원리(friendship principle)'에 대해서 배우게 될 것이다.

  private:로 상속받으면, 모든 접근 권한은 private:로 바뀐다. protected:로 상
속받으면, 모든 접근 권한은 protected:로 바뀐다. public은 베이스 클래스의
접근 권리를 그대로 유지한다. 아래의 예를 보자.

#include <stdio.h>

class CBase {

private:

    int a;

    void F() { printf("F() %d\n",a); }

protected:

    int b;

    void G() { printf("G() %d\n",b); }

public:

    int c;

    void H() { printf("H() %d\n", c); }

    CBase() { a=1; b=2; c=3; }

};//class CBase

class CDerived : public CBase {

public:

    void Print();

};//class CDerived

void CDerived::Print() {

    //F();//invalid!

    G();

    H();

}//CDerived::Print()

void main() {

    CDerived i;

    //i.G();//invalid!

    i.Print();

}

  실행 결과는

    G() 2

    H() 3

이다. 소스에서 invalid!라고 표시된 부분이 왜 에러인지 설명해 보라.

접근 선언(access declaration)

  일반적이지는 않지만, 접근 선언(access declaration)을 통하여, 접근 변경
권한을 무시하는 것이 가능하다. 아래의 예를 보자.

class CBase {

public:

    int j;

};

class CDerived : private CBase {

public:

    CBase::j;//access declaration

};

  private:로 상속받았아므로, CBase의 모든 멤버는 private: 권한을 가진다.
하지만, CDerived의 public: 권한에 명시적으로 베이스 클래스의 변수 j를 명
시함으로써 j만 public 권한이 된다.

외워야 할 표

  독자들은 아래의 표를 반드시 외워야 한다. '사생활의 원리'와 '친구의 원리'
를 표와 함께 기억하여 두라.

      유도 클래스

(modifier)

베이스 클래스

(qualifer)

private

protected

public

private

n/a

n/a

n/a

protected

private

protected

protected

public

private

protected

public

             접근 권한 표: 반드시 암기하도록 한다.

   HAVE-A 관계(have-a relationship)

  다시 '뽀리가 사과나무를 쳐다보는' 문제로 돌아가 보자. 우리는 이제까지
CMan, CTree와 CApple을 설계했다. 이제 생각(CThinking) 클래스를 설계해
보자.

class CThinking {

  public:

    void Think(char* name) { printf("%s is thinking...\n",name); }

};//class CThinking

  CThinking은 오직 1개의 멤버 함수 Think()를 가진다. CMan 클래스는 생
각을 해야하므로, CThinking 클래스를 멤버로 포함할 것이다. 수정된 CMan
클래스는 아래와 같다.

class CMan {

  private:

    char name[80];

    CThinking think;

  public:

    CMan(char* sName) { strcpy(name,sName); }//생성자(constructor)

    void Thinking() { think.Think(name); }

    void Watching() { printf("%s is watching...\n", name); }

};//class CMan

  이제 CMan 클래스는 CThinking 클래스의 객체 think를 가진다(CMan
have a CThinking). 이러한 클래스 관계를 HAVE-A 관계(have-a
relationship)이라 한다. CMan이 name도 가지지만, HAVE-A 관계는 오직
클래스에 대해서만 명시한다. HAVE-A 관계는 속이 찬 화살표(solid arrow)
로 소유자 쪽에서 출발하는 화살표로 나타낸다. CMan과 CThinking의 관계는
다음 그림과 같이 나타낸다.

             HAVE-A 관계(have-a relationship): 클래스간의 소유관계는 속이 찬 화살
          표로 나타낸다. 화살표는 소유당하는 쪽으로 향한다. 화살표의 방향은
          CMan have a CThinking. 이라는 문장을 자연스럽게 반영한다. 화살의 끝
          에 명시된 1에 주목하라. 이것은 CMan이 CThinking의 객체를 오직 1개 소
          유함을 나타낸다. 이것을 one-to-one 관계라 한다.

  만약 CMan이 CThinking의 객체를 10개 소유한다면, CMan의 소스는 아래
와 같을 것이다.

class CMan {

  private:

    char name[80];

    CThinking think[10];//객체 배열(object array)

  public:

    CMan(char* sName) { strcpy(name,sName); }//생성자(constructor)

    void Thinking() {

        for (int i=0;i<10;++i)

            think[i].Think(name);

    }

    void Watching() { printf("%s is watching...\n", name); }

};//class CMan

  HAVE-A 관계가 one-to-many일 때는 클래스 계층도를 아래 그림처럼
나타낸다.

             HAVE-A 관계의 one-to-many 표현: 0개 이상은 전산 과학에서 일반적으
          로 *를 사용하여 나타낸다. 사실 *은 UML의 표기법이다.

  이제 모두가 완성된 소스를 살펴볼 때가 되었다. 우리가 설계한 문제의 클
래스 계층도는 아래와 같다.

             문제의 클래스 계층도(class hierarchy): 모든 클래스간의 HAVE-A, IS-A
          관계가 명시되었다. 하지만, 클래스간의 상호 작용을 나타내지는 못한다.
          이 그림을 보고 클래스를 설계할 수는 있어도, 문제를 풀 수는 없을 것이
          다.

  자 아래의 예를 보자.

#include <stdio.h>

#include <string.h>

enum COLOR { BROWN=0, BLACK=1, RED=2 };

////////////////////////////////////////////////////////////////

// CThinking

class CThinking {

  public:

    void Think(char* name) { printf("%s is thinking...\n",name); }

};//class CThinking

////////////////////////////////////////////////////////////////

// CMan

class CMan {

  private:

    char name[80];

    CThinking think;

  public:

    CMan(char* sName) { strcpy(name,sName); }//생성자(constructor)

    void Thinking() { think.Think(name); }

    void Watching() { printf("%s is watching...\n", name); }

};//class CMan

////////////////////////////////////////////////////////////////

// CTree

class CTree {

  private:

    int color;

  protected:

    int height;

  public:

    CTree(int c,int h) { color=c; height=h; }

    void Grow() { printf("Tree is growing...\n"); }

};//class CTree

////////////////////////////////////////////////////////////////

// CApple -> CTree

class CApple : public CTree {

  private:

    int apple,leaf;

  public:

    CApple(int c,int h) : CTree(c,h) { apple=leaf=0; }

    void Ripe() { printf("%d apple is ripe\n",apple); }

    void Grow();//override CTree::Grow()

};//class CApple

void CApple::Grow() {//CTree::Grow()를 오버라이딩하였다.

    printf("Apple Tree is growing...\n");

    apple+=5;

    leaf+=10;

    height+=1;

}//CApple::Grow()

void main() {

    CMan a("신뽀리");

    CApple appleTree(BROWN,100);

    appleTree.Grow();

    a.Watching();

    appleTree.Grow();

    appleTree.Ripe();

    a.Thinking();

}//main()

  실행 결과는

    Apple Tree is growing...

    신뽀리 is watching...

    Apple Tree is growing...

    10 apple is ripe

    신뽀리 is thinking...

이다. 실행 결과는 만족할 만하다. 하지만, 클래스 계층도에는 객체간의 상호
작용을 기술되지 않았기 때문에, main()에서 어떤 식으로 객체의 멤버 함수를
호출해야 할지는 클래스 설계와는 전혀 다른 문제이다. 이것을 나타낼 방법은
없을까?

  놀랍게도 이것을 설명할 모델은 1970년대 초반에 형식적인 이론으로 거의
다 완성되었다. 오토마타(automata)를 모르고 프로그래머라고 말하지 말
라! 오토마타는 아무리 강조해도 지나치지 않다!!!

   다른 모델, 오토마타(automata)

  오토마타(automata)는 문제를 풀이하는 과정을 문제를 풀이하는 상태의
변화로 모델링한다. 이러한 상태 변화 기계(state transition machine)가 완
성되면 이 기계는 적절한 입력에 대해 상태를 변화하며 반응한다. 이 상태 변
환 기계를 자동 기계(automatic machine) 혹은 오토마타라 한다. 그리고 완
성된 오토마타는 상태 변화도(state transition diagram)로 나타낸다.

  형식적인 정의를 피하여 오토마타를 정의해 보자. 이것은 상태 집합
(states), 시작 상태(start state), 끝 상태 집합(final states) 및 상태 천이
함수(state transition function)로 이루어진다.

  상태(state)는 오토마타가 문제를 풀이하는 과정 중의 특별한 한 상황이며
상태를 구분하는 정수(integer)를 사용하여 원 안에 나타낸다■. 상태가 출력을

             여기서 필자가 설명하는 오토마타는 밀리(Mealy)와 무어(Moore) 오토마타
          를 결합한 형태이며, 대부분 표준 기호와 용어를 사용하였다.

하는 경우, 상태 번호(state number)와 출력을 슬래시(/)로 구분한다.

             상태(state): 원 안에 상태 번호를 적는다. 상태가 출력을 가지는 경우, 상
          태 번호와 출력을 /로 구분한다. 이 그림은 오토마타가 상태 3일 경우, 출
          력으로 a를 가지는 것을 의미한다.

  상태 집합 중에서 특별한 1개의 상태는 시작 상태(start state)이며, 기계
의 초기 상태가 된다. 즉, 오토마타가 동작을 시작하는 상태이다. 또한, 오토마
타가 종료하는 상태는 1개 이상 존재하며, 종료 상태(final state)라 한다.

  시작 상태는 원으로 향하는 화살표 혹은 캐럿(carot)을 사용하여 구분하며,
종료 상태는 이중 원(double circle)으로 나타낸다.

             시작 상태(start state)와 종료 상태(final state): 기계가 동작을 시작하는 상
          태를 시작 상태라 하고, 화살표로 추가적인 표시를 한다. 기계가 정상적으
          로 동작을 마친 상태를 종료 상태라 하고, 이중 원으로 표시한다.

  상태 천이 함수(state transition function)는 상태와 입력을 보고, 다음
상태로 천이하는 함수⼆이다. 즉 기계의 다음 상태는 현재 상태와 현재 입력으

             상태를 S, 입력을 I라 하면 이러한 함수는 수학적으로 S×I→S로 나타낸
          다.

로 결정한다. 이러한 상태 변화를 화살표(arrow)로 나타낸다. 출력이 있는 경
우 입력과 출력을 슬래시(/)로 구분한다.

             상태 변화(state transition): 상태 변화는 화살표로 나타낸다. 첫 번째 그림
          은 입력 a에 대해 다른 상태로의 변화를, 두 번째 그림은 입력 a에 대해
          출력으로 b를 가지는 상태 변화를 나타낸다.

  아래의 그림은 어떤 오토마타의 일부(상태 변화도의 일부)이다.

             상태 변화도: 오토마타는 상태 변화도로 나타낼 수 있다(물론 상태 변화
          도가 오토마타를 나타내는 유일한 방법은 아니다). 7번 상태에서 가능한
          입력은 a와 b이다. 입력 a에 대해 상태는 8로 변한다. 입력 b에 대해 오토
          마타는 'transition'을 출력하고, 상태는 9로 변한다. 오토마타가 상태 8에 진
          입했을 때, 오토마타는 'real'을 출력한다. 우리가 설계하는 오토마타는 결
          정적(deterministic)이다. 임의의 상태에서 1가지 입력에 대해서 다음 상태는
          유일하게 결정할 수 있어야 한다.

  현재 상태가 7이라 하자. 입력으로 a가 들어오면, 8번 상태로 바뀐다. 그리
고 'real'을 출력한다. 입력으로 b가 들어오면, 'transition'을 출력한다. 그리고
9번 상태로 바뀐다. 입력으로 a, b이외의 입력이 들어오면, 오토마타의 상태는
바뀌지 않는다⼆.

             오토마타에 명시된 입력 이외의 입력을 처리하는 방법은 설계자에 따라
          서 정의될 수 있다. 위의 예에서 a, b이외의 입력에 대해 ① 상태 변화가
          없거나, ② 에러 처리를 하는 경우를 생각해 볼 수 있다. 문제에 따라 적
          당한 가정을 하면 된다.

  풀이 가능한 모든 문제를 오토마타로 풀이할 수 있을까? 놀랍게도 이 질문
에 대한 대답은

YES!

이다. 사실 이것은 당연한 것이다. 모든 문제의 풀이 과정은 본질적으로 구분
가능한 1개 이상의 상태로 구성되어 있는 것이다!

  언제 오토마타를 이용하는가?

  알고리즘(algorithm)⼆을 표현하는 방법으로 흔히 순서도(flow chart)를 생

             문제를 풀이하는 체계적인 방법을 알고리즘이라 한다. 예를 들면, 1에서
          n까지의 합을 구하는 프로그램에 대해서 생각해 보자. 어떤 사람은 for문을
          이용하여, 어떤 사람은 합의 공식((n(n+1))/2)을 이용하여 프로그램을 코딩할
          것이다. 이때 문제를 풀이하는 이러한 방법을 알고리즘이라 한다. 그러므
          로 한가지 문제에 대해 해답을 제시하는 알고리즘은 1개 이상 존재할 수
          있다. 알고리즘의 성능(throughput)은 속도와 공간의 효율성(speed &
          space efficiency)으로 평가한다. 일반적으로 속도와 공간의 효율성은 반비
          례한다. 알고리즘에 대해서는 http://kepler.kyungpook.ac.kr을 참고하라.

각하지만, 오토마타의 상태 변화도는 알고리즘을 표현하는 매우 강력한 수단
이다.

  조언은 다음과 같다.

문제가 주어졌을 때, 다음의 사항들을 생각해 본다.

    (1) 문제를 풀이하는 과정이 구분할 수 있는 상태를 가지고 있는가?

    (2) 정의된 어떤 상태에서 다음 입력에 의해 상태를 바꾸는 것이 구

    현 가능한가?

위의 조건들이 만족되면 그 문제를 상태변화도로 그리는 것이 가능하며, 또한
구현 가능하다.

  상태 변화도는 다음과 같이 그린다. 상태는 원안에 번호로 나타내고, 입력
및 상태변화에 의한 출력은 화살표에 딱지(label)로 나타낸다. 또한 상태에 도
착했을 때의 입력 및 출력은 원의 딱지로 나타낸다. 시작 상태는 짧은 화살표
딱지를 붙이고, 종료 상태는 이중원(double circle)으로 나타낸다. 딱지는 a /
b의 형태로 쓰는데 입력이 a이고 출력은 b라는 것을 의미한다.

  상태 변화도를 그리는 것은 체계적인 방법이 없다. 경험에 의한 휴리스틱
(heuristic)에 의존하므로, 많은 연습을 통해서 익숙해 져야 한다.

  아래 그림과 같은 상태 변화도를 완성했을 때, 이를 구현하는 방법에 대해
서 알아보자(상태 변화도 자체를 그리는 방법은 아래의 예에서 알아본다). 구
현하는 방법 역시 형식적인 어떤 방법이 존재하는 것은 아니지만, 여기서 제
시된 방법은 독자들 자신만의 휴리스틱을 개발하는 데 많은 도움이 될 것이
다. 이 그림의 오토마타가 어떤 언어를 인식하는지, 무엇을 하는지 알려고 할
필요는 없다. 오토마타가 주어졌을 때 이를 어떻게 구현하는지에 집중해 주기
바란다.

             구현을 설명하기 위해 제작된 오토마타

  ① 먼저, 상태 유지와 입력에 필요한 변수를 선언하고, 상태에 도달했을 때
해야할 일들을 함수로 작성한다. 상태 유지를 위해 state, 입력 변수는 input,
오토마타가 사용하는 변수 s, 상태 2에 도달했을 때 해야 할 일들은
PrintOnState2()로 구현한다.

#include <stdio.h>

#include <conio.h>

#include <string.h>

void PrintOnState2(char *s)

{

    printf("%s\n",s);

    printf("terminate\n");

}//PrintOnState2

void main()

{

    int state,input=0;

    char s[80]={0,};//NULL 문자열로 초기화한다.

}//main

  ② 문제에 맞는 종료조건을 정의한 다음, while 루프와 switch 문장을 사용
하여 상태변화를 구현한다. 루프에 들어가기 전에 입력 값을 적당히 초기화하
고 시작상태를 초기화한다. 오토마타에 명시되지 않은 입력이 들어온 경우 탈
출 상태를 정의한 다음 while 루프의 전체의 탈출 조건을 명시한다. 루프를
탈출한 후에 종료상태에서 종료한 경우 해야할 일들을 while 루프 다음에 명
시한다.

#include <stdio.h>

#include <conio.h>

#include <string.h>

void PrintOnState2(char *s)

{

    printf("%s\n",s);

    printf("terminate\n");

}//PrintOnState2

void main()

{

    int state,input=0;

    char s[80]={0,};

    state=0;//시작상태는 0이다.

    while (input!=27 && state!=99) {//ESC가 입력되거나 Dead State가

                                       //되면 종료한다.

        input=getch();

        switch (state) {

            case 0:

                break;

            case 1:

                break;

            case 2:

        }//switch

    }//while

    if (state==2)//2번은 종료상태이다.

        printf("Input recognized\n");

}//main

  ③ 각각의 상태에 대해 가능한 모든 입력에 대해 switch 문의 case를 완성
한다. 예를 들면 상태 0인 경우 'a'가 입력으로 들어오면,

    s=s+'a';

    print "start";

에 해당하는 문장을 C++로 다음과 같이 코딩한다.

    strcat(s,"a");

    printf("%s\n",s);

  완성된 코드는 다음과 같다.

#include <stdio.h>

#include <conio.h>

#include <string.h>

void PrintOnState2(char *s)

{

    printf("%s\n",s);

    printf("terminate\n");

}//PrintOnState2

void main()

{

    int state,input=0;

    char s[80]={0,};

    state=0;

    while (input!=27 && state!=99) {

        input=getch();

        switch (state) {

            case 0:

                if (input=='a') {

                    strcat(s,"a");

                    printf("start\n");

                    state=1;

                } else

                    state=99;//Dead State를 99로 정의하였다.

                break;

            case 1:

                if (input=='b') {

                    strcat(s,"b");

                    state=1;

                } else if (input=='c') {

                    strcat(s,"c");

                    state=2;

                    PrintOnState2(s);

                } else

                    state=99;

                break;

            case 2:

                PrintOnState2(s);//상태에 도착할 때마다 실행된다.

                if (input=='d') {

                    printf("restart\n");

                    state=0;

                } else

                    state=99;

        }//switch

    }//while

    if (state==2)

        printf("Input recognized\n");

}//main

  위의 프로그램은 이 절에서 그림으로 제시한 오토마타를 완벽하게 구현한
것이다!

   오토마타 설계: 예(example)

  이제 오토마타 자체를 설계하는 방법에 대해 예를 통해 알아보자. 실제, 문
제의 오토마타를 설계했다면, 문제를 풀이한 것이다. 그림으로 그려진 오토마
타를 구현하는 것은 오토마타의 설계에 비해 - 비록 시간과 노동은 들더라도
- 사소한(trivial) 것이다.

   벽(wall)에 부딪히면 튕기는 공(bouncing ball)을 오토마타로 설계하
라.

             튕기는 공: 공이 진행하다가 벽에 부딪히면 입사각에 의해 결정되는 반사
          각 방향으로 튕긴다. 구현을 간단히 하기 위해 공의 진행 방향을 8방향으
          로 제한하자.

   이러한 예는 벽돌 격파 게임 등에서 흔히 찾아 볼 수 있다. 더 간단
한 방법이 존재함에도 불구하고, 이것을 오토마타로 구현해 보자.

  먼저 공이라는 객체의 상태(state)를 결정하자. 공의 상태는 진행 방향에 따
라 다음과 같이 0∼7로 구분할 수 있다.

             공의 상태: 진행 방향에 따라 0에서 7까지로 구분한다. 이러한 상태의 설
          정은 문제의 성격에 따라 결정할 수 있다.

  다음에 공에 가해지는 입력을 결정하자. 공의 상태를 바꾸는 것은 공이 벽
에 부딪쳤을 때이다. 그러므로, 사방(four sides)에 존재하는 벽을 공의 입력으
로 설계할 수 있다. 각각의 입력을 left, top, right 와 bottom으로 설정하자.

             입력의 설정: 공이 벽에 부딪히는 순간은 공의 상태를 변경하는 유일한
          입력이다. 공의 입장에서 벽의 방향에 따라 left, top, right와 bottom으로 결
          정하자.

  자, 이제 상태와 입력을 결정했으므로, 오토마타를 구현해 보자. 일단, 공의
상태가 3일 때, top 벽에 부딪히는 경우를 고려해 보자.

             상태 3일 때 top의 입력: 공의 상태는 5가 되어야 한다.

  top에 부딪히면 3으로 진행하던 공은 분명 5로 진행해야 한다. 이것은 다음
과 같은 오토마타로 구현할 수 있다.

             구현한 일부 오토마타: 3의 상태에서 공은 현재 위치에서 화면 좌표계
          (screen coordinate system)의 (-1,-1) 방향으로 진행한다. 이러한 진행을 계
          속하다가 입력이 top이면, 상태는 5로 바뀐다. 이 때 공은 (-1,+1)방향으로
          진행해야 한다.

  공의 위치를 나타내기 위해 화면의 좌측 상단(left top)을 원점으로 하는 화
면 좌표계(screen coordinate system)를 사용한다고 하자. 또한, move x,y
는 공을 현재 위치에서 (x,y)만큼 옮기는 함수라고 가정하자. 3의 상태에서

move -1,-1

을 반복한다. 진행하다가 윗 벽에 부딪히면 입력이 top인 경우이므로, 공은

move -1,+1

로 진행해야 한다. 이것은 공의 상태가 3에서 (입력 top에 대해) 5로 바뀌는
것을 나타낸다. 완전한 오토마타는 실습문제를 참고하라.

   객체 모델과 오토마타의 결합

  이제 객체 모델과 오토마타를 결합할 모든 준비가 된 것 같다. 본론에 들어
가기 전에 객체 지향 언어(OOL: object oriented language)가 갖추어야 하
는 '추상화(abstraction), 캡슐화(encapsulation), 다형성(polymorphism)과
상속성(inheritance)'에 대해서 정리해 보자.

  추상화란 객체의 자세한 세부 동작을 모르고도, 객체를 사용할 수 있는 기
능을 말한다. 사실 추상화는 C의 함수를 통해서도 어느 정도 제공되었다. 하
지만, C++의 클래스는 좀 더 높은 수준의 추상화를 제공한다.

  클래스 사용자가 객체를 사용하기 위해서 알아야 할 것은 함수의 인터페이
스(interface) 정도이다. 멤버 함수의 자세한 동작은 몰라도 그 객체를 사용할
수 있는 것이다!

  캡슐화는 추상화의 동전의 양면이다. 추상화가 잘 된 객체는 외부로부터 보
호된다. 객체에서 private: 멤버는 (친구를 제외하곤) 접근할 수 없다. 캡슐화
란 공개해야할 것만 공개하고, 숨겨야 할 것은 숨기는 것을 의미한다. 이렇게
잘 설계된 클래스는 외부로부터 안전하게 보호되어, 단단한 캡슐에 쌓인 것
같다!

  다형성은 같은 인터페이스가 문맥에 따라 다른 역할을 하는 것을 말한다.
함수의 이름이 같더라도 파라미터가 다르면, 같은 이름의 함수를 사용할 수
있다. 연산자의 기능을 문맥에 따라 다르게 동작하도록 정의할 수 있다. 템플
릿(template)은 다형성의 극치이다!

  상속성은 이미 정의된 클래스를 다시 사용하는 기능이다. 상속성은
Windows 라이브러리인 MFC와 OWL에서 그 위력이 빛난다!

  C++는 완벽하게 추상화, 다형성과 상속성을 지원한다. C++는 이 세상에서
(아직까지는) 가장 완벽한 OOL인 것이다!

  이제 '뽀리와 사과나무'의 완벽한 장면을 살펴보자.

             뽀리와 사과나무 문제의 진행: CMan의 객체가 2개 필요하다. CApple이 1
          개 필요하다. 이들간의 관계를 오토마타로 나타낸다.

  3개의 객체를 가정하자. 각각의 이름은 '신뽀리', '아저씨'와 '사과나무'이다.
'신뽀리'의 상태를 3가지로 구분하자.

0: 사과나무를 쳐다보면 생각하는 상태

1: 아저씨에게 말하는 상태

2: 그녀를 생각하는 상태

  '아저씨'의 상태를 2가지로 구분하자.

0: 말거는 것을 기다리는 상태

1: 대답하는 상태

  '사과나무'의 상태를 1가지로 구분하자.

0: 계속 자라면서 열매가 익어가는 상태

  신뽀리의 상태 1은 아저씨의 상태를 0에서 1로 바꾼다. 즉, '아저씨'에 가해
지는 입력은 '신뽀리'에 의해서 발생한다. 신뽀리의 상태 변화는 자신의 내부
상태에 의해서 결정된다. 사과나무는 0의 상태에서 Grow()와 Ripe()를 반복한
다.

  자 어떤가? 독자들은 이 모두를 오토마타로 나타낼 수 있는가?

   다중 상속(multiple inheritance)

  다중 상속에 관한 주제는 '가상 함수(virtual function)'에서 심도 깊게(^:^)
다룬다.

   실습문제

1. 오토마타가 C++의 소스(source)를 컴파일(compile)하는데 어떻게 사용할 수
있는지 설명하고, 컴파일러의 스캐너(scanner)에 대해서 설명하라.

2. 튕기는 공(bouncing ball)의 오토마타를 모두 완성하라.

3. HWP의 Alt+D로 '선 그리기'를 선택했을 때의 동작을 오토마타로 구현하
라.

4. 탁상용 계산기(calculator)의 오토마타를 설계하고, 이를 구현하라.

5. 한/영 자동 전환기를 오토마타로 어떻게 구현할 수 있는지 설명하고, 이를
구현하라.

6. '뽀리와 사과나무'의 오토마타를 완성하라.

7.


38. 생성자(constructor), 소멸자(destructor)

38. 생성자(constructor),
소멸자(destructor)

  클래스의 멤버 함수(member function)중 생성자(constructor)와 소멸자
(destructor)는 아주 특별한 종류의 멤버 함수이다. 생성자는 객체가 메모리
에 할당된 직후 ①처음으로 ②단 한번 ③불려지는 함수이다.

  ①처음으로: 메모리에 할당된 - 인스턴스(instance)화된 - 객체의 멤버 함
수 중 생성자가 제일 먼저 자동으로 호출된다. 이것은 일반 변수의 선언과 객
체의 선언을 상당히 구분되게 만든다. 변수의 선언은 함수를 호출하지 않지만,
객체의 선언은 생성자 함수를 호출한다.

  ②단 한번: 객체의 생성자는 한 번 이상 호출되지 않는다. 여기에 의문이
있는 독자가 있는가? 독자의 의문은 정당한 것이다. 실제로 생성자는 2번 이
상 불려질 수 있는데, 복사 생성자(copy constructor)와 잘못된 클래스 계층을
가지는 구조에서의 상위 클래스의 생성자가 그러하다■. 또한, 임시 객체를 만
들기 위해 생성자를 명시적으로 호출하는 것이 가능하다. 하지만, 일반적으로
생성자는 단 한 번 불려야 한다.

             이 부분은 각각 아래의 절에서 설명하는 복사 생성자와 '가상 함수'를 참
          조하라.

  ③불려지는: 생성자는 컴파일러에 의해서 자동으로 호출되므로, 일반 함수
를 호출하듯이 특정 객체의 생성자를 직접 호출할 수 없다. 하지만, 임시 객체
를 만들기 위해 명시적인 생성자 호출을 사용한다면(권장되는 방법은 아니지
만) 생성자를 직접 호출하는 것이 가능하다.

  소멸자는 객체가 메모리에서 해제되기 바로 전 ①마지막에, 자동으로 ②불
려지는 함수이다.

  ①마지막으로: 객체가 메모리에 남아있는 동안, 마지막 순간에 불린다. 소
멸자 호출 이후에 객체는 메모리에서 해제된다. 하지만, - 일반적인 것은 아
니지만 - 사용자가 객체의 소멸자를 명시적으로 호출한다면, 소멸자 호출 이
후에 객체는 메모리에서 해제되지 않을 수도 있다.

  ②불려지는: 소멸자는 생성자처럼 명시적인 호출 없이도 객체가 메모리에서
사라지기 전에 자동으로 불린다. 하지만, 명시적으로도 특정 객체의 소멸자를
호출할 수 있다.

  생성자와 소멸자는 멤버 함수가 가지는 대부분의 특징을 가진다. 생성자와
소멸자의 특징은 다음과 같다.

  ① 생성자는 오버로딩 할 수 있다.

  ② 생성자는 디폴트 파라미터를 가질 수 있다.

  ③ 소멸자는 파라미터를 가질 수 없다. 그러므로, 오버로딩 할 수도 없다.

  ④ 소멸자는 가상 소멸자(virtual destructor)로 선언될 수 있다. 하지만, 생성
자는 가상이 될 수 없다.

  ⑤ 일반적인 것은 아니지만, 생성자와 소멸자는 명시적인 호출이 가능하다.

  독자들이 생성자와 소멸자에 관해서 프로그램할 때 가장 주의해야 하는 사
항은, 객체의 생성자의 호출 횟수와 소멸자의 호출 횟수가 다른 경우의, 메모
리 샘(memory leak) 문제 - 이것은 동적 메모리 할당이나, 클래스 계층 구
조에 의해서 발생할 수 있다 - 이다. 하지만, 먼저 일반적인 경우에 대해서
살펴보자.

  CTest라는 간단한 클래스를 만들어 보자. 이 클래스는 멤버 변수 i를 가지
며, 멤버 함수로 Show()를 가진다. 클래스는 아래와 같다.

class CTest {

    int i;

public:

    void Show() {

        cout << i << endl;

    }

};//class CTest

  CTest의 생성자를 추가해 보자. 생성자는 클래스 이름과 같은 함수 이름을
가지고, 임의의 파라미터를 가질 수 있다. 이 중 특별히 파라미터가 없는 생성
자(혹은 파라미터가 있더라도 모두 디폴트 값을 가지는 생성자)는 디폴트 생
성자(default constructor)라 한다. 디폴트의 의미는, 객체를 만들 때 생성자
에 대한 명시적인 파라미터가 없어도 불려진다는 것을 뜻한다. 우리가 추가할
생성자는 파라미터를 1개 가지며, 멤버 변수 i를 설정한다. 또한 호출 순서를
확인하기 위해 "constructor" 문자열을 출력한다.

class CTest {

    int i;

public:

    CTest(int ti) {

        i=ti;

        cout << "constructor" << endl;

    }

    void Show() {

        cout << i << endl;

    }

};//class CTest

  생성자의 리턴 타입(return type)이 없음에 주목하라. 하지만 생성자가 명시
적으로 호출된다면, 생성자는 자신의 클래스 타입을 리턴한다. 생성자 함수를
인라인(inline)■으로

             클래스 안에서 정의된 함수는 자동으로 인라인 함수(inline function)가 된
          다. 인라인 함수는 if 같은 제어문장을 사용할 수 없다. '인라인 함수'를 참
          고하라.

정의하지 않고, 클래스의 바깥에 정의하는 경우, 멤버 함수■이므로 스코프 해

             멤버 함수(member function)와 함수(function)는 다르다. 보기에는 조금 다
          르지만, 실제적으로 컴파일러가 생성하는 코드에는 보다 많은 차이가 있
          다. 독자들은 다음의 규칙을 기억하는가? "멤버 함수는 함수가 아니다."
          그러므로, 멤버 함수를 정의할 때, 범위 해결사(::)가 반드시 필요하다.

결사(::)를 사용하여 클래스 범위(class scope)를 명시해야 한다.

class CTest {

    int i;

public:

    CTest(int ti);

    void Show() {

        cout << i << endl;

    }

};//class CTest

CTest::CTest(int ti) {

    i=ti;

    cout << "constructor" << endl;

}

  이제 소멸자를 추가해 보자. 소멸자는 클래스 이름과 같은 함수이름을 가지
지만, 함수 이름 앞에 비트 부정 연산자(bitwise NOT operator) 기호인, 틸드
(tilde: ~)■를 추가해야 한다. 생성자와는 달리 파라미터를 가질 수 없고, 리

             C++의 설계자인 Bjarne Stroustrup은 소멸자 앞에 붙이는 기호를 결정하기
          위해 얼마나 고민했을까? 아마도 부정을 의미하는 !와 ~를 두고 고민했을
          것이다. 필자의 생각에도 !CTest() 보다는 ~CTest()가 보기에 좋다.

턴 타입도 명시할 수 없다. 소멸자는 단순히 "destructor"를 출력한다.

class CTest {

    int i;

public:

    CTest(int ti);

    ~CTest■() {

             소멸자가 클래스의 외부에서 정의되는 경우 CTest::~Ctest() 처럼 정의되어
          야 한다.

        cout << "destructor" << endl;

    }

    void Show() {

        cout << i << endl;

    }

};//class CTest

CTest::CTest(int ti) {

    i=ti;

    cout << "constructor" << endl;

}

  이제 클래스가 완성되었다. CTest 객체를 만들어 보자. CTest객체의 생성
자가 반드시 1개의 파라미터를 가지므로

CTest t;

처럼 사용할 수 없다.

CTest t(5);

처럼 사용한다. t(5)가 마치 함수 호출처럼 보이지 않는가? 실제로 그렇다. 하
지만, 함수 t()를 호출하는 것이 아니라, 객체 t의 생성자 CTest(int ti)를 호출
한다■. 아래 프로그램의 출력결과에 주목하라.

             생성자의 이러한 문법 때문에 객체가 아닌 일반 형(type)의 변수에 대해서
          도 int i=3;을 int i(3); 처럼 초기화하는 것이 가능하다. '함수형 초기화'를 참
          고하라. 그렇다면 반대로 CTest t(5); 를 CTest t=5; 처럼 초기화하는 것은
          어떤가? 가능하다. 하지만, 생성자의 파라미터가 2개 이상인 경우는 반드
          시 괄호를 사용해야 한다.

#include <iostream.h>

#include <stdio.h>

class CTest {

    int i;

public:

    CTest(int ti);

    ~CTest() {

        cout << "destructor" << endl;

    }

    void Show() {

        cout << i << endl;

    }

};//class CTest

CTest::CTest(int ti) {

    i=ti;

    cout << "constructor" << endl;

}

void main() {

    CTest t(5);

    t.Show();

    cout << "main" << endl;

}

  출력 결과는

    constructor

    5

    main

    destructor

이다. main()을 종료하기 전 객체 t가 스택에서 해제되면서 "destructor"를 출
력하였다. 비록 "main" 이후에 "destructor"가 출력되었지만, "destructor"를 출
력한 시점은 "main"을 종료하기 전이다.

  전역 객체가 만들어진 경우 프로그램의 실행 결과는 더욱 놀랍다. 시작하는
함수는 - 멤버 함수까지 포함하면 - main()이 아니라 객체의 생성자가 된다!
아래 프로그램의 출력결과에 주목하라.

#include <iostream.h>

#include <stdio.h>

class CTest {

    int i;

public:

    CTest(int ti);

    ~CTest() {

        cout << "destructor" << endl;

    }

    void Show() {

        cout << i << endl;

    }

};//class CTest

CTest::CTest(int ti) {

    i=ti;

    cout << "constructor" << endl;

}

CTest t(5);

void main() {

    t.Show();

    cout << "main" << endl;

}

  출력 결과는

    constructor

    5

    main

    destructor

이다. 비록 같은 결과이기는 하지만, "constructor"는 main()실행 전에 호출된
객체의 생성자에 의해 출력된 것이다. 얼마나 놀라운가!

new를 사용한 경우의 생성자 호출

  객체의 포인터가 선언된 이후, new를 사용하여 동적으로 객체를 생성하는
경우 생성자는 언제 호출되며, 파라미터는 어떻게 설정할까? 예를 들어

CTest* p;

가 선언되었을 때, p가 객체를 가리키도록 해 보자. new를 사용하여 다음과
같이 적는다.

    CTest* p;

    p = new CTest(5);

  new 다음에 객체의 형(type)인 CTest가 명시되었으며, 파라미터는 클래스
이름에 연속하는 괄호안에 명시된다. 이제 *p - p는 객체가 아니다 - 의 멤버
함수 Show를 호출하기 위해

(*p).Show();

혹은

p->Show();

를 사용할 수 있다. new를 사용하여 동적으로 메모리를 할당했으므로, delete
를 사용하여 명시적으로 객체가 사용한 메모리를 해제한다.

delete p;

  이제 아래 프로그램의 실행 중 메모리 상태를 차례대로 그려보자.

#include <iostream.h>

class CTest {

    int i;

public:

    CTest(int ti);

    ~CTest() {

        cout << "destructor" << endl;

    }

    void Show() {

        cout << i << endl;

    }

};//class CTest

CTest::CTest(int ti) {

    i=ti;

    cout << "constructor" << endl;

}

void main() {

    CTest* p;

    p=new CTest(5);

    p->Show();

    cout << "main" << endl;

    delete p;

}

  결과는

    constructor

    5

    main

    destructor

이다.

             main()의 CTest* p가 [1000]∼[1003]번지에 할당된다. new에 의해 힙에
          CTest의 객체가 만들어진다. 생성자가 호출되어 CTest의 데어터 멤버 i는
          5로 초기화된다.

             할당문(p=new CTest(5))에 의해 p가 객체의 시작 주소를 가리킨다. delete
          p; 는 할당된 객체를 운영체제에 반납한다. main()이 종료할 때 p도 스택에
          서 팝될 것이다.

   p=new CTest(5)와 p=new CTest[5]는 어떻게 다른가?

   전자는 CTest의 객체 1개를 만들며, 생성자의 파라미터가 5로 지정된
경우이다. 후자는 CTest의 객체 5개를 만드는 문장이며, 생성자의 파라미터는
명시되지 않았다. 그러므로 후자는 문법적인 에러이므로 컴파일 시간 에러가
발생한다. 객체의 배열을 만들면서 파라미터를 설정할 수는 없을까? 이것은
밑의 계속되는 절(section)들에서 다룬다.

  생성자의 오버로딩(overloading)

  다른 함수나 멤버 함수처럼 생성자도 오버로딩할 수 있다. 이제 좀더 세련
된 클래스를 설계해 보자. 우리가 설계할 CInt 클래스는 데이터 멤버로 두개
의 char 멤버 high와 low를 가진다. 그리고 필요에 따라서 high와 low가 각각
상위 바이트와 하위 바이트로■ 결합된 int 형태로 사용하기를 원한다.

             32비트 운영체제에서 int는 4바이트이다. 필자가 이 책을 처음 쓸 때는
          Borland C++ 4.5로 프로그램을 작성하였는데, 그것은 Win16용이었기 때문
          에 - 비록 Win32를 지원하기는 했지만 - int는 2바이트였다. 하지만, Visual
          C++ 5.0및 최근 버전의 각 컴파일러는 Win32에서 동작하기 때문에 int는 4
          바이트이다.

  생성자는 2개의 버전이 존재한다. 하나는 high와 low를 각각 설정하며, 다
른 하나는 int를 파라미터로 받아 상위 바이트를 high에 하위 바이트를 low에
설정한다.

#include <iostream.h>

#include <stdio.h>

class CInt {

    char high,low;

public:

    CInt(char h,char l) {

        high=h;

        low=l;

    }

    CInt(int i) {

        low=(char)(i&0x000000ff);

        high=(char)((i>>8)&0x000000ff);

    }

    char GetHigh() { return high; }

    char GetLow() { return low; }

    int GetInt() {

        int i=high;

        i<<=8;

        i|=low;

        return i;

    }

};//class CInt

void main() {

    CInt i(2,3),j(0x0405);

    printf("%x,%x\n",i.GetInt(),j.GetHigh());

}

  CInt i(2,3)은 첫 번째 생성자를 호출한다. CInt j(0x0405)는 int를 파라미터
로 받는 두 번째 생성자를 호출한다. 두개의 char를 int로 결합하기 위해 비트
연산자를 사용하고 있다. 출력결과는

203,4

이다. CInt에 디폴트 생성자는 없다. 독자들은 디폴트 생성자를 추가하기 위
해 두 번째 생성자를 다음과 같이 수정할 수 있다.

    ...

    CInt(int i=0) {

        low=(char)(i&0x000000ff);

        high=(char)((i>>8)&0x000000ff);

    }

    ...

  아직도 CInt가 2개의 생성자를 가지는 것처럼 보이지만, 컴파일러는 3개의
생성자를 생성한다. 각각은

    CInt(char,char);

    CInt(int);

    CInt();

이다. 그러므로, 객체를 생성할 때, 파라미터는 위 3개중 1개와 일치해야하며,
3번째 디폴트 생성자인 경우

    CInt k();

처럼 호출하는 것은 불가능하며,

    CInt k;

처럼 호출해야 한다■.

             CInt k();는 CInt의 객체 k를 만드는 것이 아니라, CInt를 리턴하는 함수 k()
          를 선언하는 문장이다. 즉 CInt k(void); 의 의미이다.

아래의 예제를 참고하라.

#include <iostream.h>

#include <stdio.h>

class CInt {

    char high,low;

public:

    CInt(char h,char l) {

        high=h;

        low=l;

    }

    CInt(int i=0) {

        low=(char)(i&0x000000ff);

        high=(char)((i>>8)&0x000000ff);

    }

    char GetHigh() { return high; }

    char GetLow() { return low; }

    int GetInt() {

        int i=high;

        i<<=8;

        i|=low;

        return i;

    }

};//class CInt

void main() {

    CInt i(2,3),j(0x0405),k;

    printf("%x,%x,%x\n",i.GetInt(),j.GetHigh(),k.GetInt());

}

  결과는

203,4,0

이다.

  소멸자의 오버로딩?

  소멸자는 오버로딩이 불가능하다. 왜 그런가?

  소멸자는 컴파일러에 의해서 자동으로 호출되므로, 소멸자가 불리는
문맥(context)을 사용자가 지정할 수 없다. 즉, 파라미터를 명시할 수 없으므
로 오버로딩 자체가 불가능하다. 소멸자의 명시적인 호출이 가능하므로, 소멸
자에 파라미터를 지정할 수 없는 것이 조금은 불만일지도 모른다. 하지만, 소
멸자의 명시적인 호출 자체가 매우 특별한 일이며, 그것을 위해 소멸자의 파
라미터를 지정하는 오버헤드를 감수할 수는 없다.

  함축적인 변환(implicit conversion)

  생성자 파라미터가 1개인 경우, 객체 선언문에서 생성자를 호출하기 위해
대입문(assignment statement)을 사용할 수 있다. 하지만, 이것은 대입문으로
번역되는 것이 아니라, 컴파일 시간에 생성자 호출로 변환된다. 이것을 함축
적 변환(implicit conversion)이라 한다.

  함축적 변환은 코드를 읽기 어렵게 만들므로 권장하지는 않는다.

  선언문이 아니라, 이미 만들어진 객체에 대입문이 사용된 경우, 생성자가 호
출된다. 하지만, 컴파일러가 임시로 객체를 만든 다음, 이 객체의 생성자가 호
출되고, 이 후에 객체의 내용이 복사되는 것이므로, 주의 깊게 사용해야 한다.

  함축적 변환이 일어나지 않도록, 생성자를 선언할 수 있는데, 키워드
explicit을 사용한다. explicit은 계속되는 절(section)에서 설명한다. 아래의 함
축적 변환의 예를 참고하라.

#include <iostream.h>

#include <string.h>

class X {

    int age;

    char name[80];

  public:

    X(int);

    X(const char*, int = 0);

};

X::X(int a) {

    age=a;

    cout << "construct X(int)" << endl;

}

X::X(const char* n,int a) {

    strcpy(name,n);

    age=a;

    cout << "construct X(const char*,int)" << endl;

}

void main() {

  X a = 1■;//대입문처럼 보이지만, 생성자 호출로 함축적으로 변환된다.

             물론 이 문장은 X a={1}; 혹은 X a(1);처럼 쓰는 것이 가능하다.

  X b = "Jessie";

  a = 2;//이 문장이 생성자를 호출한다는 것을 주의하라.

}

  실행 결과는

    construct X(int)

    construct X(const char*,int)

    construct X(int)

이다.

  생성자와 소멸자는 직접 호출할 수 없다?

  일반적인 것은 아니지만, 생성자와 소멸자를 명시적으로 호출하는 것이 가
능하다. 아래의 클래스를 예로 들어 설명해 보자.

class CTest {

    int i;

public:

    CTest(int ti) {

        i=ti;

        cout << "constructor\n";

    }

    ~CTest() {

        i=0;

        cout << "destructor\n";

    }

    void Inc() {

        ++i;

        cout << i << endl;

    }

};//class CTest

  CTest의 생성자는 데어터 멤버 i를 설정하고 "constructor"를 출력한다. 소
멸자는 데이터 멤버 i를 0으로 초기화하고, "destructor"를 출력한다. 소멸자에
i=0; 이란 문장은 이상해 보인다. 객체가 사라지는 마당에 웬 초기화? 그리고
멤버 함수 Inc()는 데이터 멤버 i를 1증가시키고 i값을 출력한다. 이제 main()
에 드라이버 루틴(driver routine)을 만들어 보자.

...

void main() {

    CTest t(3);

    t.Inc();

}

  예상대로 위 프로그램의 출력결과는

    constructor

    4

    destructor

이다. 객체를 초기화시킬 목적으로, 이미 만들어진 객체의 생성자를 다시 부르
는 방법은 없을까? 없다. 하지만, 임시 객체를 만들어 그 객체를 이미 만들어
진 객체에 복사해 주는 방법으로 이러한 문제를 해결할 수 있다. 아래의 프로
그램 코드 조각을 살펴보자.

...

CTest t(3);

t.Inc();

...

CTest temp(3);

t=temp;

...

  위의 예에서 CTest의 객체 temp는 단지 객체 t를 초기화시키기 위한 목적
으로 다시 만들어진 것이다. 이렇게 하면, temp의 생성자가 호출될 것이고, 깨
끗한 이 객체의 내용을 t에 복사하면 객체 t를 초기화하는 것이 가능하다.

  여기에서 주목해야 하는 것은 임시 객체 temp이다. 이 객체는 단지 초기화
를 위해 선언되고 만들어졌을 뿐이다. 메모리 측면에서 이것은 낭비이다. 그래
서 임시 객체(temporary object)를 만들어 객체 t에 내용을 대입하고 임시
객체를 메모리에서 자동으로 해제되는 방법이 필요할 것이다. 아래의 예를 참
고하라. 명시적으로 생성자를 호출하여, 임시 객체를 만들고 있다. 프로그램의
출력 결과에 주목하라.

#include <iostream.h>

class CTest {

    int i;

public:

    CTest(int ti) {

        i=ti;

        cout << "constructor\n";

    }

    ~CTest() {

        i=0;

        cout << "destructor\n";

    }

    void Inc() {

        ++i;

        cout << i << endl;

    }

};//class CTest

void main() {

    CTest t(3);

    t.Inc();

    t.Inc();

    t=CTest(3);//명시적인 생성자 호출은 임시 객체를 생성한다. 임시 객체

                //또한 생성자이므로, 일반 객체가 생성되듯이 생성자와 소

                //멸자가 호출된다.

    t.Inc();

    //t.CTest::~CTest();

    //t.Inc();

}

  출력 결과는

    constructor

    4

    5

    constructor

    destructor

    4

    destructor

이다. t=CTest(3); 이란 문장에 주목하라. 이 문장은 임시 객체를 만들어 이
임시 객체의 내용을 t로 복사한다. 임시 객체를 만들 때 생성자와 소멸자가
호출된다. 출력 결과 중 중간의 "constructor", "destructor"는 임시 객체의 생
성자와 소멸자가 호출될 때 실행된 결과이다. 이 프로그램의 메모리 상태를
그려보는 것은 매우 도움이 될 것이다. 프로그램의 출력 결과를 참고하여 독
자들이 직접 그려보기 바란다.

  소멸자는 어떤가? 소멸자를 명시적으로 호출하는 것은 가능하며, 가능하다
면 어떤 의미인가? 소멸자를 호출하기 위해

~CTest();

를 호출하는 것은 무의미하다. 소멸자는 이미 객체가 만들어져 있어야 하는데,
~CTest() 란 문장은 어느 객체의 소멸자가 호출되는지 알지 못한다. 그렇다면

t.~CTest();

는 어떤가? t의 소멸자를 호출하려는 시도이다. 이것은 타당하다. 그리고 실제
로 객체 t(물론 t는 CTest형이다)의 소멸자가 호출된다■. 아래의 프로그램 결

             이러한 소멸자 호출을 가짜 소멸자 호출(pseudo destructor call)이라 한
          다.

과를 살펴보라.

#include <iostream.h>

class CTest {

    int i;

public:

    CTest(int ti) {

        i=ti;

        cout << "constructor\n";

    }

    ~CTest() {

        i=0;

        cout << "destructor\n";

    }

    void Inc() {

        ++i;

        cout << i << endl;

    }

};//class CTest

void main() {

    CTest t(3);

    t.~CTest();■

             t.CTest::~CTest(); 처럼 사용하는 것도 가능하다.

    t.Inc();

}

  결과는

    constructor

    destructor

    1

    destructor

이다. 만들어진 객체는 1개이지만, 소멸자는 2번 호출되었다. 물론 이것은 위
험한 사용이다. 만약 생성자에서 힙 메모리를 할당하고, 소멸자에서 할당된 힙
메모리를 해제하는 코드가 있다면, 이 프로그램에서는 메모리 샘(leak)이 발생
할 것이다.

  explicit: 명시적 생성자 호출

  1997년 11월 결정된 ISO C++표준에는 몇 개의 새로운 키워드가 추가되었
다. explicit, mutable, typename과 namespace가 그것이다. 각각은 explicit을
제외한 각각은 '선언 변경자', '메타 클래스(meta class; template)'와 '이름
공간'에서 자세히 다룬다.

  explicit은 함축적 변환이 일어나지 않게 생성자를 선언하기 위해 사용한다.
일반적으로 생성자의 파라미터가 1개인 경우는 대입문을 사용하여 생성자를
호출하는 것이 가능하다. 이것은 실제로 생성자로 호출로 변환되는데 이러한
변환을 함축적 변환(implicit conversion)이라 한다. 이러한 함축적 변환은
코드를 읽기 어렵게 만들므로 때때로 바람직하지 못하다. explicit 키워드를
사용하면 이러한 함축적 변환은 컴파일 시간 에러로 발생한다. 아래의 예를
참고하라.

#include <iostream.h>

#include <string.h>

class X {

    int age;

    char name[80];

  public:

    explicit X(int);//생성자 앞의 explicit은 함축적 변환을 방지한다.

    explicit X(const char*, int = 0)■;

             explicit 키워드는 파라미터가 1개인 생성자에만 사용될 수 있다는 사실에
          주의하라. 만약 두 번째 파라미터 int가 디폴드 값을 가지지 않는다면,
          explicit은 문법 에러이다.

};

X::X(int a) {

    age=a;

    cout << "construct X(int)" << endl;

}

X::X(const char* n,int a) {

    strcpy(name,n);

    age=a;

    cout << "construct X(const char*,int)" << endl;

}

void main() {

  X a = X(1);//만약 X a = 1;이라고 쓴다면 컴파일 시간 에러가 발생한다.

  X b = X("Jessie");

  a = X(2);

}

  실행 결과는

    construct X(int)

    construct X(const char*,int)

    construct X(int)

이다■.

             위 소스는 볼런드 C++ 5.0, 비주얼 C++ 4.2이상에서 실행된다. 볼런드
          C++ 4.x 이하의 버전에서는 explicit, mutable, typename 및 namespace등의
          키워드를 지원하지 않는다.

  객체 배열(object array)에서 생성자 호출

  기본 형(basic type)의 배열을 만들듯이 구조체(structure)나 클래스(class)
같은 복합형(compound type)의 배열을 만드는 것이 가능하다. 특히 클래스
형의 배열이 만들어졌을 때를 객체 배열(object array)이라 한다.

  객체 배열에서는 어떻게 생성자를 호출할까? 위의 예로든 CTest의 객체를
5개 만들려고 한다. 그러면 다음과 같은 선언문을 가질 것이다.

CTest a[5];

  하지만, 이 선언은 타당하지 않다. CTest는 디폴트 생성자가 없으므로, 반
드시 생성자의 파라미터가 명시되어야 함에도 불구하고, 생성자의 파라미터가
명시되지 않은 것이다. 만약 CTest가 디폴트 생성자를 가진다면, 위 문장은
에러가 아니다. 그러므로 a[0]부터 a[4]의 5개의 객체에 대해 생성자를 명시해
야 한다. 이 때 함축적 변환은 유용하게 사용된다.

CTest a[5]={1,2,3,4,5};

  비록 위 문장이 일반 배열을 초기화하는 것처럼 사용되었지만, 이것은 생성
자 호출의 함축적 변환이다. a[0]을 위해 CTest(1), a[4]를 위해 CTest(5)의
생성자가 호출된다. 아래의 예를 참조하라.

#include <iostream.h>

class CTest {

    int i;

public:

    CTest(int ti) {

        i=ti;

        cout << "constructor\n";

    }

    ~CTest() {

        i=0;

        cout << "destructor\n";

    }

    void Inc() {

        ++i;

        cout << i << endl;

    }

};//class CTest

void main() {

    CTest a[5]={1,2,3,4,5};//{}안의 값은 함축적 변환을 거쳐 생성자에

                            //차례대로 전달된다.

    int i;

    for (i=0;i<5;++i)

        a[i].Inc();

}//main

실행 결과는

    constructor

    constructor

    constructor

    constructor

    constructor

    2

    3

    4

    5

    6

    destructor

    destructor

    destructor

    destructor

    destructor

이다.

  그러면 생성자가 파라미터를 2개 이상 받는 경우는 어떻게 생성자의 파라미
터를 명시하는가? 예를 들어 CTest의 생성자가 2개의 정수 파라미터를 받는
다고 가정하자. 독자들은 다음과 같이 객체 배열의 생성자를 명시하려 시도할
지 모른다.

CTest a[5]={{1,10},{2,20},{3,30},{4,40},{5,50}};//error!

혹은

CTest a[5]={(1,10),(2,20),(3,30),(4,40),(5,50)};//error!

과 같은 시도는 그럴듯하게 보이지만, 모두 에러이다. 객체 배열에서 생성자
호출은 클래스의 이름을 포함하여야 한다. 바른 문장은 다음과 같다.

CTest a[5]={CTest(1,10),CTest(2,20),CTest(3,30),CTest(4,40),CTest(5,50)};

아래의 예를 참고하라.

#include <iostream.h>

class CTest {

    int i,j;

public:

    CTest(int ti,int tj) {

        i=ti; j=tj;

        cout << "constructor\n";

    }

    ~CTest() {

        i=0; j=0;

        cout << "destructor\n";

    }

    void Inc() {

        ++i; ++j;

        cout << i << " " << j << endl;

    }

};//class CTest

void main() {

    CTest a[5]={CTest(1,10),CTest(2,20),

                CTest(3,30),CTest(4,40),CTest(5,50)};

      //객체 배열에서 생성자 호출은 완전하게 명시해야 한다.

    int i;

    for (i=0;i<5;++i)

        a[i].Inc();

}//main

결과는

    constructo

    constructo

    constructo

    constructo

    constructo

    2 11

    3 21

    4 31

    5 41

    6 51

    destructor

    destructor

    destructor

    destructor

    destructor

이다. 물론 생성자의 파라미터가 1개인 첫 번째 예의 경우도 함축적 변환을
사용하지 않고 다음과 같이 명시할 수 있다.

CTest a[5]={CTest(1),CTest(2),CTest(3),CTest(4),CTest(5)};

  이와 같은 객체 배열의 초기화가 정당한 것은 사실이지만, 권장되는 방법은
아니다. 만약 객체 배열의 크기가 100이라면, 생성자는 100번 명시되어야 한
다! 그러므로, 객체 배열로 사용될 클래스를 설계할 작정이라면, 디폴트 생성
자를 만드는 것이 좋다. 권장 사항은 다음과 같다.

"객체 배열로 사용될 클래스를 설계할 때는 반드시 디폴트 생성자를
만들라"

  이러한 권장 사항은 객체 배열을 동적으로 만들 경우 선택의 여지가 없다.
만약

    CTest* p;

    p=new CTest[5];

처럼 동적으로 CTest의 객체를 만든다고 가정해 보자. 어떻게 생성자의 파라
미터를 명시하는가?

p=new CTest[5]={1,2,3,4,5};//error!

혹은

    p=new CTest[5]={CTest(1),CTest(2),CTest(3),CTest(4),CTest(5)};

와 같은 문장은 모두 그럴듯해 보이지만, 문법 에러이다. 디폴트 생성자가 없
는 클래스의 객체를 동적 배열로 만들 수 있는 방법은 없다■.

             특별하게 new/delete를 오버로딩한다면, 전혀 불가능한 것은 아니다.

  복사 생성자(copy constructor)

  객체를 함수의 파라미터로 전달하는 경우를 생각해 보자. 예전의 C에서 구
조체를 함수에 전달할 때, 구조체를 전달하기보다는 구조체의 포인터를 전달
했다. 그것은 구조체의 크기가 클수록 이점이다. 크기가 1024바이트인 구조체
의 경우, 구조체를 모두 전달한다면, 스택에 1024바이트가 모두 푸시(push)되
어야 한다. 하지만, 구조체의 포인터를 전달한다면, 4바이트 포인터만 스택에
푸시하면 되는 것이다! 그래서 어떤 컴파일러는 구조체가 포인터로 전달되지
않는 경우 경고 메시지(warning message)를 발생하는 선택사항(option)을 가
지고 있다.

             볼런드 C++ 4.5의 Project Options 대화상자: 구조체가 값으로 전달된 경우
          경고 메시지를 출력하는 것을 설정하는 대화상자를 제공한다.

  C++에서 객체가 전달되는 경우도 같은 상황을 생각해 볼 수 있다. 객체를
모두 전달하는 것보다는 객체의 포인터만을 전달하는 것이 확실히 이득이다.
물론 객체의 내용이 바뀌기를 원치 않는다면 조심스럽게 함수의 몸체를 코딩
해야 할 것이다.

  하지만, 분명히 객체가 전달되어야 하는 경우가 있다. 이 때 구조체와는 다
른 미묘한 문제가 발생한다. 함수의 파라미터로 전달되는 객체는 이미 메모리
에 생성되어 있을 것이다. 그리고, 이 객체가 파라미터로 전달될 때는 이미 만
들어진 객체의 내용이 스택에 복사되어 전달될 것이다. 이 때 생성자가 호출
되어야 하는가? 파괴자는 어떤가? 아래의 예를 참고하라. CStr클래스는 문자
열을 관리한다. 생성자는 동적으로 메모리를 할당하여, 문자열을 복사하며, 파
괴자는 동적으로 할당된 메모리를 해제한다. 문자열의 시작 위치는 str이 가리
킨다. 멤버 함수 Print()는 문자열 str을 출력한다. main()에서는 객체 s를 만
들어 문자열을 "hello"로 초기화한다. 객체 s는 함수 f()에 전달되며, s의 문자
열이 출력된다. 컴파일 시간 에러는 없다. 하지만, 실행 시간 에러(run-time
error)가 발생한다. 어디가 잘못인가?

#include <stdio.h>

#include <string.h>

#include <conio.h>

class CStr {

    char *str;

  public:

    CStr(char *s="") {

        str=new char[strlen(s)+1];

        strcpy(str,s);

        printf("constructor\n");

    }

    ~CStr() {

        delete[] str;

        printf("destructor\n");

    }

    void Print() { printf("%s\n",str); }

};//class CStr

void f(CStr s) {

    s.Print();

}

void main() {

    CStr s("hello");

    f(s);

}

  실행 결과는

    constructor

    hello

    destructor

    RUN-TIME ERROR!

이다. 즉 프로그램은 정상적으로 종료하지 않고 Win95에서 GPF■(general

             이 에러는 응용 프로그램이 자신이 할당하지 않은 메모리를 접근하려 했
          을 때 발생한다. Win95는 다음과 같은 에러 메시지를 출력한다. "이 프로
          그램이 잘못된 연산을 수행하여 종료합니다."

protection fault)를 발생시킨다. 3번째의 destructor는 언제 출력된 것일까? 이
것은 main()에서 s객체가 파괴될 때가 아니라, 함수 f()에서 복사된 객체 s가
파괴될 때 실행된 것이다. 마지막의 에러는 main()이 객체 s의 파괴자를 호출
할 때 발생한다. 이미 해제된 메모리를 해제하려 한 것이다!

  자 이 프로그램을 자세히 따라가 보자. 결론적으로 말하자면,

"복사 생성자는 아무리 강조해도 지나치지 않다."

             CStr s("hello");: main()함수의 객체 선언에 의해 객체 s가 스택에 만들어진
          다. s의 멤버 str은 아직 초기화되지 않았다. 이제 메모리가 할당되었으므
          로 생성자가 호출될 것이다.

             str=new char[strlen(s)+1];: 생성자가 호출된다. 6바이트를 동적으로 할당한
          다. 시작 주소를 [1000]이라 가정하자. 파라미터로 전달된 "hello"는 C컴파
          일러가 문자열을 위해 관리하는 메모리에 잠시 복사되어 있다. s의 멤버
          str은 [1000]으로 설정된다.

             strcpy(str,s);: 임시 메모리에 있는 문자열 s가 str이 가리키는 곳에 복사된
          다. 끝에 \0도 같이 복사된다는 것에 주의하라. 그리고 printf()에 의해
          "constructor"가 출력된다.

             f(s);: 함수 f()로 객체 s가 전달된다. 객체 s의 내용이 그대로 복사되어
          (bitwise copy) 스택이 1개의 복사본이 만들어진다. 5000번지의 s는 main()의
          객체 s이며, 4996번지의 s는 f()의 객체 s이다. s의 내용이 복사된 것이지
          생성된 것이 아니므로, s의 생성자는 호출되지 않는다. f()의 str역시 [1000]
          을 가리키고 있다는 사실에 주목하라.

             s.Print();: 멤버 함수 CStr::Print()가 호출되어 "hello"가 출력된다. 이제 함수
          f()를 빠져 나간다. f()의 객체 s가 스택에서 해제되기 전, 파괴자가 호출된
          다. 객체는 메모리에서 사라지기 바로 전 항상 파괴자가 호출된다는 사실
          을 상기하자. 파괴자는 1000번지를 해제한다. 이제 1000번지는 유효한 메
          모리 공간이 아니다. 여기에 핵심이 있다. 파라미터로 전달된 객체의 생성
          자는 호출되지 않지만, 파괴자는 항상 호출된다.

             Dangling pointer: 이제 main()의 객체 s의 멤버 str이 가리키는 곳은 무효한
          (invalid) 곳이다. 무효한 곳을 가리키는 포인터를 댕글링 포인터(dangling
          pointer)라 한다. main()이 종료하기 직전, 객체 s가 스택에서 해제되기 전
          파괴자가 호출될 것이고, 파괴자는 1000번지를 메모리 해제하려 할 것이
          다. 이것은 실행시간 에러를 유발한다.

  이 문제에 대한 해결책은 2가지가 있다.

    ■ 객체를 전달하지 않고 포인터를 전달한다.

    ■ 복사 생성자(copy constructor)를 만들어 둔다.

  ① 첫 번째 해결책은 함수 f()를 다음과 같이 수정하는 것이다.

    ...

    void f(CStr& s) {

        s.Print();

    }

  만약 함수 f()를 void f(CStr* s) { s->Print(); }처럼 수정한다면, main()에
서는 f(s); 를 f(&s);로 수정하여야 할 것이다. 규칙은 다음과 같다.

"생성자에서 동적으로 메모리를 할당하는 객체를 함수의 파라미터로
전달할 때는 항상 레퍼런스를 전달하라."

  ② 더 일반적인 해결책은 복사 생성자(copy constructor)를 만드는 것이
다. 복사 생성자는 객체가 만들어질 때가 아니라, 이미 만들어진 객체가 다른
곳으로 복사될 때 호출되는 생성자를 의미한다. 객체가 복사되는 경우는 ①함
수의 파라미터 전달, ②대입문 및 ③오버로드된 연산자가 객체를 리턴하는 경
우 등에 발생한다.

  복사 생성자는 일반 생성자와 다르므로, 특별한 문법 규칙을 필요로 한다.
복사 생성자를 정의하는 방법은 다음과 같다.

    ■ 반드시 자신의 상수 클래스 형의 레퍼런스를 파라미터로 받는다.

  그러므로 CTest의 복사 생성자는

CTest::CTest(const CTest& s) {...

처럼 정의⼆될 것이다. 위 문장은 s가 변경되지 못함과, s의 레퍼런스가 전달

             이것은 실제로 CTest 클래스를 CTest 클래스로 형 변환하는 사용자 정의
          형 변환(user-defined conversion)을 정의한 것이다. 예를 들면, class X를
          class Y로 형 변환하기 위해서는 class Y의 멤버 함수에 Y(const X& x); 를
          추가하면 된다. 이것에 관한 사항은 '새로운 형태의 형변환'에서 설명한다.

됨을 지시한다. 기억하려 두라! 복사 생성자는 항상 위와 같이 첫 번째 파라
미터만이 명시되어야 한다.

  자 그러면, 이 예에서 복사 생성자는 어떻게 정의되어야 할까? 객체 s가 복
사될 때 str이 가리키는 내용 역시 복사되도록 생성자를 정의하여야 할 것이
다.

    CStr (const CStr &s) {//copy constructor

        str=new char[strlen(s.Get())+1];

        strcpy(str,s.Get());

        printf("copy constructor\n");

    }

CStr::Get()은 단순히 str을 리턴한다. 단, s가 상수이므로, Get()역시 상수로
선언되어야 할 것이다. const변경자에 관해서는 'const 변경자'를 참조하라.
수정된 소스는 다음과 같다.

#include <stdio.h>

#include <string.h>

class CStr {

    char *str;

  public:

    CStr(char *s="") {

        str=new char[strlen(s)+1];

        strcpy(str,s);

        printf("constructor\n");

    }

    CStr (const CStr &s) {//copy constructor

        str=new char[strlen(s.Get())+1];

        strcpy(str,s.Get());//const 객체는 const 멤버 함수만을 호출할 수

                            //있다.

        printf("copy constructor\n");

    }

    ~CStr() {

        delete[] str;

        printf("destructor\n");

    }

    char *Get() const {//함수 뒤의 const는 이 함수가 어떠한 멤버도 변경

                       //시켜서는 안됨을 지시한다.

        return str;

    }

    void Print() { printf("%s\n",str); }

};//class CStr

void f(CStr s) {

    s.Print();

}

void main() {

    CStr s("hello");

    f(s);

}

  실행 결과는

    constructor

    copy constructor

    hello

    destructor

    destructor

이다. 이제 실행시 에러 GPF는 더 이상 발생하지 않는다.

             f(s);: 이제 main()의 s가 복사될 때 복사 생성자가 호출된다. 복사 생성자
          는 새로 메모리를 할당하여 main()의 s의 멤버 str이 가리키는 str을 복사한
          다. 새로 할당된 메모리를 [1006]이라 하자. main()의 s는 [1000]을 가리키고,
          f()의 s는 [1006]을 가리킨다.

             파괴자 호출: f()에서 호출된 파괴자는 [1006]을 해제한다.

             main()의 s: main()의 s가 가리키는 1000번지는 여전히 유효하다. str은 더
          이상 댕글링이 아니다.

  복사 생성자가 호출되는 경우

  복사 생성자가 정의되었을 때, 복사 생성자가 호출되는 경우는 어떤 경우일
까? 복사 생성자는 아래의 3경우 외에는 호출되지 않는다.

    ■ 함수의 파라미터로 객체의 전달: 이미 만들어진 객체가 함수의

    파라미터로 전달될 때, 객체가 스택에 복사되면서 복사 생성자가 호

    출된다. 위의 예에서 f(s); 같은 경우가 이에 해당한다. 함수 f()의 프

    리픽스(prefix) 코드는 객체 s를 구성하면서, 복사 생성자를 호출한다.

    ■ 함수가 객체를 리턴: 함수가 객체를 리턴하는 경우, 임시 객체가

    대입문에 의해 복사될 때, 복사 생성자가 호출된다. 예를 들어 함수

    f()의 원형이 CStr f(CStr s); 인 경우 CStr v("hello"),u; u=f(v); 의

    문장에서 함수 f()가 호출될 때, 함수 f()가 리턴되어 임시 객체가 u

    에 복사될 때 복사 생성자는 2번 호출된다.

    ■ 객체의 선언에 사용된 대입연산자: 객체를 선언하면서 초기화하

    는 대입 연산자는 복사 생성자를 호출한다. 예를 들면, CStr

    s("hello"); CStr t=s; 에서 두 번째 객체 선언문 t=s;는 복사 생성자

    를 호출한다.

   CStr s("hello"); CStr t=s; 는 복사 생성자를 호출하지만, CStr
s("hello"); CStr t; t=s;는 복사 생성자를 호출하지 않는다는 사실을 주의하
라. 선언문에 사용된 대입 연산자와 일반 대입 연산자는 다르게 번역된다. 그
렇다면 t=s;는 어떻게 동작할까? 이러한 대입문은 비트 단위 복사(bitwise
copy)가 이루어진다. 즉, s의 내용은 1비트도 틀림없이 그대로 t로 복사된다.

  대입 연산자가 비트 단위 복사로 번역되는 것은 심각한 문제를 야기한다.
아래의 예는 논리적 에러를 포함하고 있다. 결과를 보지 말고, 문제점을 찾아
보라!

#include <stdio.h>

#include <string.h>

class CStr {

    char *str;

  public:

    CStr(char *s="") {

        str=new char[strlen(s)+1];

        strcpy(str,s);

        printf("normal constructor\n");

    }

    CStr (const CStr &s) {//copy constructor

        str=new char[strlen(s.Get())+1];

        strcpy(str,s.Get());

        printf("copy constructor\n");

    }

    ~CStr() {

        delete[] str;

        printf("destructor\n");

    }

    char *Get() const { return str; }

    void Print() { printf("%s\n",str); }

};//class CStr

void f(CStr s) {

    s.Print();

}

void main() {

    CStr s("hello"),

         t("world");

    CStr u=s;//객체의 선언문에 사용된 대입 연산자는 복사 생성자를 호출한

             //다.

    f(u);//함수의 파라미터로 전달된 객체는 복사 생성자를 호출한다.

    s=t;//객체 간의 대입문은 복사 생성자를 호출하지 않는다. 다만 그대로

        //복사(bitwise copy)된다. 그러므로, 이 문장은 논리적 오류이다.

    s.Print();

}

실행 결과는

    normal constructor

    normal constructor

    copy constructor

    copy constructor

    hello

    destructor

    world

    destructor

    destructor

    destructor//이 문장이 출력되기 전 에러가 발생한다.

가 될 것이라고 예상했겠지만, 불행하게도 마지막의 "destructor"는 출력되지
않는다■. 왜 그러한 일이 발생하는가? 아래의 메모리 맵을 자세히 따라가 보

             만약 이 소스가 DOS 환경에서 실행되었다면, 컴파일러에 따라 실행시 에
          러가 발생하지 않을 수도 있다. 하지만, 그것은 보호를 전혀 행하지 않는
          DOS 운영 체제의 에러이다. 분명 프로그램은 잘못이다. 원도우 콘솔
          (Console)모드로 컴파일되는 비주얼 C++, C++ 빌더 등을 사용한다면, 위
          소스는 분명히 GPF를 발생한다.

자.

             CStr s("hello"),t("world");: 객체 s와 t가 만들어지고, 생성자가 호출되어 문
          자열을 위한 메모리를 동적으로 할당한 후, "hello"와 "world"를 각각 복사
          한다.

             CStr u=s;: 객체 u를 위한 복사 생성자가 호출된다. s의 str이 가리키는
          "hello"가 복사되어 u의 str이 가리키는 곳으로 복사된다.

             f(u);: main()의 객체 u가 f()의 객체 s로 복사되면서 s의 복사 생성자가 호
          출된다. 이제 힙에 "hello"는 3곳에 "world"는 1곳에 존재한다.

             f()의 종료: 함수 f()가 리턴되기 전, 객체 s가 메모리에서 해제되므로, 파괴
          자가 호출된다. 1018번지는 메모리 해제된다.

             s=t;: 객체 t가 s로 비트 단위 복사된다. 원래 s가 가지던 내용은 t의 내용
          으로 바뀌어진다. 원래 s의 str이 [1000]에서 [1006]으로 바뀌어지는 것에 주
          목하라. 이제 힙에 할당된 1000번지를 가리키던 포인터는 존재하지 않는
          다.

             s.Print();: 이제 s의 str은 [1000]의 "hello"를 가리키지 않는다. [1006]의
          "world"를 가리키므로, "world"가 출력된다.

             main()의 종료: main()이 종료하기 전 u,t와 s의 순으로 객체가 파괴될 것이
          다. u는 1012번지를 해제한다. t는 1006번지를 해제한다. s가 이미 해제된
          [1006]을 다시 해제하려 하므로, 실행시간 에러가 발생한다. 또한, [1000]의
          6바이트는 해제되지 않은 채로 남아 있다.

  왜 이와 같은 문제가 발생하는가? 그것은 객체의 대입문이 비트 단위 복사
(bitwise copy)를 수행하기 때문이다. 동적으로 메모리를 관리하는 객체 사이
의 대입문은 특별하게 코딩되어야 한다.

  이 예에서는 ① s가 이미 가리키는 [1000] 번지를 해제한 후, ② 다시 메모
리를 할당하여 ③ t의 str이 가리키는 문자열을 대입하는 방식을 사용해야 할
것이다. 그렇다면, 대입문의 역할을 바꿀 수는 없을까? 그렇다! 바로 =연산자
를 오버로드하는 것이다. =연산자는 아래와 같은 기능을 수행한다.

    ...

        ① delete[] str;

        ② str=new char[strlen(s.Get())+1];

        ③ strcpy(str,s.Get());

        printf("= operator\n");

        return *this;

             =연산자의 바른 동작: 먼저 이전 메모리를 해제한다. 그리고 새로운 메모
          리를 할당한다. 마지막으로 문자열을 새로운 메모리로 복사한다.

  자, 이제 복사 생성자를 사용하는 완벽한 규칙을 제시해 보자. 이 규칙은
C++에서 무지무지 중요한 규칙이다.

복사 생성자 규칙(copy constructor rule)

"생성자에서 동적으로 메모리를 할당하는 경우, 반드시 복사 생성자를
만들라. 또한, 반드시 대입 연산자(=)를 오버로딩하여, 레퍼런스를
리턴하도록 하라!"

  이제, 복사 생성자 규칙을 완벽히 따르는 완벽한 CStr 클래스의 소스는 아
래와 같다. 오버로드된 =연산자가 레퍼런스를 리턴하지 않아도 되지만, 그것
은 분명히 비효율적이다. 레퍼런스를 리턴하는 경우와 그렇지 않은 경우의 출
력 결과를 비교하라.

#include <stdio.h>

#include <string.h>

class CStr {

    char *str;

  public:

    CStr(char *s="") {

        str=new char[strlen(s)+1];

        strcpy(str,s);

        printf("normal constructor\n");

    }

    CStr (const CStr &s) {//copy constructor

        str=new char[strlen(s.Get())+1];

        strcpy(str,s.Get());

        printf("copy constructor\n");

    }

    ~CStr() {

        delete[] str;

        printf("destructor\n");

    }

    char *Get() const { return str; }

    void Print() { printf("%s\n",str); }

    CStr& operator=(CStr &s) {//return reference to object

    //CStr operator=(CStr &s) {//return object itself

        delete[] str;

        str=new char[strlen(s.Get())+1];

        strcpy(str,s.Get());

        printf("= operator\n");

        return *this;

    }

};//class CStr

void f(CStr s) {

    s.Print();

}

void main() {

    CStr s("hello"),

         t("world");

    CStr u=s;

    f(u);

    s=t;

    s.Print();

}

■ 출력 결과: =연산자가 레퍼런스를 리턴하는 경우, 즉 소스가 CStr&
operator=(CStr &s) 인 경우.

    normal constructor

    normal constructor

    copy constructor

    copy constructor

    hello

    destructor

    = operator

    world

    destructor

    destructor

    destructor

■ 출력 결과: =연산자가 레퍼런스를 리턴하지 않는 경우, 즉 소스가 CStr
operator=(CStr &s) 인 경우.

    normal constructor

    normal constructor

    copy constructor

    copy constructor

    hello

    destructor

    = operator

    copy constructor//복사와 대입의 여분의 오버헤드가 필요하다.

    destructor

    world

    destructor

    destructor

    destructor

  위의 예에서 보다시피, 오버로드된 연산자가 객체를 리턴하는 경우, 대부분
레퍼런스를 리턴하는 것이 효율적이다.

  베이스 클래스와 멤버 객체의 생성자 호출

  아래 그림과 같은 클래스 계층 구조를 생각해 보자.

             클래스 계층도: B는 A의 하위 클래스(sub class)이다. 또한 B는 C를 객체
          데이터 멤버로 가진다. 클래스 B의 객체가 만들어 질 때, A의 생성자와 C
          의 생성자가 호출되어야 한다.

  클래스 B를 위해, 클래스 A는 베이스 클래스로 사용되었다. 또한 B는 클래
스 C를 포함(HAVE-A)하고 있다. 상속 관계(IS-A)를 나타내기 위해 속인 빈
화살표(hollowed arrow)를 사용했으며, 포함관계(HAVE-A)를 나타내기 위
해 속인 찬 화살표(solid arrow)를 사용했다는 것에 주목하라.

  클래스 B의 객체가 만들어질 때 클래스 A,C의 생성자는 언제 호출되며, 어
떻게 파라미터를 설정하는가? 이것은 C++의 생성자 호출문법에 의해 가능하
며, 베이스 클래스의 생성자 호출을 위해 심벌 콜론(:)을 사용한다■.

             콜론(:)은 클래스 정의에서 상속 관계를 나타내기 위해, 생성자에서 베이
          스 클래스의 생성자를 호출하기 위해 사용된다. 또한 생성자에서 상수형
          멤버를 초기화하기 위해, 객체 멤버의 생성자를 호출하기 위해, 참조형 데
          이터 멤버를 초기화하기 위해 사용된다.

  그러므로, 클래스 B의 생성자를 정의할 때, 베이스 클래스 A와 객체 멤버
C에 대한 생성자 호출 문법이 명시되어야 한다. 그것은 다음과 같이 생성자
헤더와 블락({})의 사이에 콜론(:)을 사용하여 뒤에 명시한다.

    B::B() : A(value1), C(value2) {

    ...

    }

  위의 예는 B의 생성자가 호출되기 전, ① A의 생성자, ② C의 생성자가 호
출될 것을 지시한다. B의 생성자는 마지막에 호출된다■. : 뒤에 명시되는 순

             파괴자는 생성자가 불린 반대 순서로 불린다.

서에 주의하라. 콤마(,) 연산자의 관례처럼 표현식은 왼쪽에서 오른쪽으로 평
가된다. 만약

    B::B() : C(value2), A(value1) {

    ...

    }

처럼 선언했다면, C의 생성자가 A의 생성자보다 먼저 호출된다. 이처럼 베이
스 클래스와 멤버 객체의 생성자는 원래 생성자의 헤더 뒤에 콜론(:) 다음에
명시한다. 아래의 예를 참고하라.

#include <iostream.h>

class A {

    int a;

public:

    A(int ta) {

        a=ta;

        cout << "constructor A" << endl;

    }

    ~A() { cout << "destructor A" << endl; }

    void Print() { cout << a << endl; }

};//class A

class C {

    int c;

public:

    C(int tc) {

        c=tc;

        cout << "constructor C" << endl;

    }

    ~C() { cout << "destructor C" << endl; }

    void Print() { cout << c << endl; }

};//class C

class B : public A {//클래스 상속성을 나타내기 위해 :을 사용한다.

    int b;

    C c;

public:

    B(int tb,int ta,int tc) : A(ta), c(tc) {//베이스 클래스나 멤버 객

                                          //체의 생성자를 호출하기 위해

                                          //: 을 사용한다.

        b=tb;

        cout << "constructor B" << endl;

    }

    ~B() { cout << "destructor B" << endl; }

    void Print() {

        A::Print();

        cout << b << endl;

        c.Print();

    }

};//class B

void main() {

    B b(1,2,3);

    b.Print();

}//main

실행 결과는

    constructor A

    constructor C

    constructor B

    2

    1

    3

    destructor B

    destructor C

    destructor A

이다. 실제로 콜론(:) 다음에 명시하는 것에는 다음과 같은 것이 있다. 자세한
사항은 '함수형 초기화'를 참고하라.

    ■ 상수 멤버의 초기화

    ■ 참조의 초기화

    ■ 객체 멤버의 초기화

    ■ 베이스 클래스의 생성자 호출

    ■ 일반 데이터 멤버의 초기화(권장되지는 않음)

  실습문제

1. 아래의 프로그램에는 에러가 있는가? 아니라면, 왜 그런가?

    #include <iostream.h>

    class CTest {

        int i;

    public:

        CTest (int t) { i=t; }

    };

    void main() {

        CTest i(2),j(3),k();//이 문장이 왜 에러가 아닌가?

    }

2.


범위 해결사

40. 범위 해결사(scope
resolver)

  C++의 범위 해결사(scope resolver, ::)는 우리가 생각하는 것보다 많은
역할을 가지고 있다. 대충 말한다면, '이 경우에는 무엇을 써야 할까'하고 고민
된다면, 범위 해결사를 사용하면 된다.

  범위 해결사는 다음과 같은 곳에 사용한다.

  1) 일반 함수가 아니라, 클래스의 멤버 함수임을 나타내기 위해서, 멤버 함
수를 정의(definition)할 때

  2) 전역과 지역(global and local)(변수 혹은 함수)을 구분할 때

    - 일반 함수에서 전역 변수와 지역 변수의 구분

    - 멤버 함수에서 멤버 함수의 변수와 클래스의 변수를 구분

    - 멤버 함수에서 멤버 함수와 일반 함수를 구분

  3) 클래스의 정적 멤버(static member) 혹은 열거형(enumeration) 멤버
를 접근할 때

    - 정적 멤버 변수, 열거형 멤버 변수의 접근

    - 정적 멤버 함수의 호출

  4) 클래스 상속(inheritance)에서 상위 클래스의 멤버를 접근할 때

    - 상위 클래스 멤버의 접근 권한(access authority)을 변경할 때

    - (모호함이 발생할 수 있는) 상위 클래스의 멤버를 접근할 때

  5) 이름 공간(namespace)에서 이름공간을 지정할 때

    - 특정 이름공간의 변수를 정의할 때

    - 이름 공간을 활성화 시킬 때

  만약 독자들이 위의 큰 5가지 경우에 대해서, 모두 알고 있다면, 이 장을 건
너뛰어도 좋다. 단, 이름공간에 사용된 범위 해결사는 이 장에서 설명하지 않
는다. 그것은 '이름공간'을 참고하라.

   멤버 함수의 정의

  멤버 함수를 정의할 때, 범위 해결사를 사용하는 것은 일반적인 것이다. 그
것은 클래스에 대해서, 혹은 구조체, 공용체의 멤버에 대해서도 모두 마찬가지
이다. 아래의 예를 참고하자.

#include <iostream.h>

class CTest {

public:

    int i;

    void f(int temp);//CTest의 멤버 함수 f()의 선언

    void Print(){ cout << i << endl; }

};//class CTest

void f(int temp);//일반 함수 f()의 선언

void main() {

    CTest t;

    int temp=3;

    f(temp);//일반 함수 f()의 호출

    t.f(temp);//멤버 함수 f()의 호출

    t.Print();

}//main

void f(int temp) {//일반 함수 f()의 정의

    cout << temp*temp << endl;

}

void CTest::f(int temp) {//멤버 함수 f()의 정의

    i=temp;

}

  출력 결과는

9

3

이다.

  위의 예에서 보듯이, f()와 CTest의 f()는 다르다. 독자들은 다음과 같은 규
칙을 기억하고 있는가?

"멤버 함수는 함수가 아니다"

그러므로, 멤버 함수를 정의(물론 선언할 때 적어 주어도 된다. 선언할 때 적
는 것은 선택사항(option)이지만, 정의할 때 적는 것은 필수다)할 때, 반드시
'클래스이름::'를 함수의 이름 앞에 적어 주어야 한다.

   전역과 지역의 구분

  전역 변수와 지역 변수의 범위 해결 규칙에 대해서 독자들은 이미 알고 있
을 것이다. 그것은

"전역 변수와 지역 변수의 이름이 같으면, 지역 범위(local scope)를 먼저
참고한다."

이다. 하지만, C++에서 이러한 범위 규칙을 잠시 무시하기 위해서, 범위 해결
사를 사용하는 것이 가능하다. 아래의 예를 참고하자.

#include <iostream.h>

int temp=2;//전역 변수

class CTest {

public:

    int i;//클래스의 멤버 변수

    void f(int i);//i는 f()의 지역 변수, 멤버 변수 i와 구분이 필요하
다

    void Print(){ cout << i << endl; }

};//class CTest

void f(int i);

void main() {

    CTest t;

    int temp=3;//지역 변수, 전역 변수 temp와 구분이 필요하다.

    f(temp);//지역 변수 temp를 참조한다.

    t.f(::temp);//전역 변수 temp를 참조한다.

    t.Print();

}//main

void f(int temp) {//전역 변수 temp도 아닌, main()의 temp도 아닌 f()의

                 //지역 변수 temp가 된다.

    cout << temp*temp << endl;

}

void CTest::f(int i) {

    CTest::i=i;//멤버 변수 i에 지역 변수 i를 대입한다.

                 //클래스의 멤버 함수에서, 지역 변수와 멤버 변수를 구

                 //분하기 위해서, 멤버 변수 앞에 범위 해결사를 사용한

                 //다.

    ::f(temp);//일반 함수 f()를 호출한다. 물론 파라미터 temp는 전역

                //변수(지역 변수가 없으므로)이다. 만약 범위 해결사를

                //생략하면, 프로그램은 자신을 무한히 호출하므로, 스택

                //이 넘치게 되어, 실행 시 오류가 발생한다.

}

  출력 결과는

9

4

2

이다.

  main()에서는 지역 변수 temp와 전역 변수 temp를 구분하기 위해서

::temp

처럼 범위 해결사를 사용하였다. 범위 해결사를 사용하면, 범위 규칙(scope
rule)을 무시하고, 전역 변수를 참조한다. 물론, 참조하려는 전역 변수가 없으
면, 컴파일 시간 에러가 발생한다.

  CTest의 멤버 함수 f()에서는 멤버 함수가 아닌 일반 함수 f()를 호출하기
위해,

::f(temp);

처럼 범위 해결사를 사용했다. 또한 멤버 함수 f()의 지역 변수 i와 CTest의
멤버 변수 i를 구분하기 위해

CTest::i

처럼, '클래스이름+범위해결사' 형태를 사용했다.

   정적 멤버의 접근

  정적 멤버는 그것이 함수이든, 변수이든 객체마다 메모리가 할당되지 않는
다(멤버 함수의 메모리는 객체에 어떻게 할당되는가?). 이러한 정적 멤버를
접근하기 위해서 반드시 범위 해결사를 사용해야 한다. 아래의 예를 참고하자.

#include <iostream.h>

class CTest {

public:

    CTest() { ++counter; }

    ~CTest() { --counter; }

        enum _TYPE { FIRST=1, SECOND=2, THIRD=3 };

    static int counter;

    static void PrintNumber(){ cout << counter << endl; }

};//class CTest

int CTest::counter=0;

void main() {

    CTest a,b,c;

    CTest* d;

    d=new CTest;

    cout << CTest::counter << endl;//CTest의 정적 변수 counter를

                    //접근한다. 물론 counter의 접근 권한은 public이어

                    //야 한다.

    delete d;

    CTest::PrintNumber();//CTest의 정적 멤버 함수를 호출한다.

        cout << CTest::FIRST << endl;

}//main

  실행 결과는

4

3

1

이다.

  독자들은 이러한 정적 데이터 멤버의 사용을, 클래스 안에서 정의된 정적
변수 혹은 열거형(enumeration) 변수를 접근하기 위해 빈번하게 사용하는 것
을 보게 될 것이다. 이것은 iostream의 파일 조작에서 파일 입출력 모드를 조
작하는 플래그 변수에서 찾아 볼 수 있다.

  정적 멤버 함수의 사용도 일반적인 것이다. 이것은 MFC에서 핸들을 사용
하여, 전체 객체를 만들기 위해 RUNTIME_CLASS를 사용하는 FronHandle()
함수에서 찾아 볼 수 있다(물론 이것 외에도 예는 많다).

   상속에서 범위 해결

  클래스를 상속할 때, 상위 클래스에서 protected: 로 선언된 멤버를 하위 클
래스에서 public: 으로 변경하기 위해, 범위 해결사를 사용할 수 있다. 또한,
상위 클래스의 멤버와 하위 클래스의 멤버가 이름이 같을 때, 상위 클래스의
멤버를 접근하기 위해서는 명시적으로 상위 클래스의 이름과 범위 해결사를
사용해야 한다. 아래의 예를 참고하라.

#include <iostream.h>

class CSuper {

private:

    int i;

protected:

    int j;

public:

    int k;

    CSuper() { i=1; j=2; k=3; }

    void Print() {

        cout << i << endl

            << j << endl

            << k << endl;

    }

};//class CSuper

class CSub : public CSuper {

public:

    CSuper::j;//이제 j는 CSub에서만 public이다.

    CSub() { CSuper::j=4; }

    void Print() {

        cout << "Print() of CSub" << endl;

        CSuper::Print();//상위 클래스의 Print()를 호출한다.

    }

};//class CSub

void main() {

    CSub i;

    i.Print();

    i.j=5;//j는 public: 이므로 접근할 수 있다.

    i.Print();

}//main()

  실행 결과는

    Print() of CSub

    1

    4

    3

    Print() of CSub

    1

    5

    3

이다. 물론

CSuper i;

라고 선언되었다면,

i.j=5;

라고 선언하는 것은 protected: 멤버를 접근하는 것이므로, 불법이다.

   이름공간의 범위 표시

  이름공간에서 범위 해결사는 문법적 심벌로 사용된다. 하지만, 이름 공간은
그 응용 예를 극히 찾아보기 힘들다. 필자도 소규모의 프로젝트를 진행하면서,
이름 공간을 사용한 적은 없다. 그래서 이 장에서 '이름 공간'에서 사용된 범
위 해결사는 생략한다.

  이 사항은 '이름 공간'을 참고하라.

   연습문제

1.


41. 인라인 함수(inline function)

41. 인라인 함수(inline
function)

  함수의 호출(function call)을 실제로 컴파일러가 어떻게 구현할까? 함수 호
출을 위해 컴파일러는 실제로 프리픽스(prefix)와 서픽스(suffix)를 함수의
호출 전후(before and behind)에 자동으로 생성한다(프리픽스와 서픽스는 함
수의 호출 전후에 컴파일러가 자동으로 생성하는 코드의 일부를 말한다).

   보이지 않는 오버헤드: 프리픽스(prefix)와
서픽스(suffix)

  아래의 예를 보자. main()에서 함수 Max()를 호출하고 있다. Max()는 두
개의 정수 중 큰 정수를 리턴한다.

#include <stdio.h>

int Max(int a,int b) {

    return a>b?a:b;

}

void main() {

    int a=2,b=3;

    printf("%d\n",Max(a,b));

      //함수의 호출 전후에는 CPU 레지스터들의 상태를 저장/복구하는

      //오버헤드가 필요하다.

}

  결과는

3

이다.

  함수를 호출할 때 함수를 호출하기 전의 기계 상태(machine status)⼆가

             기계 상태는 CPU 내부의 메모리의 상태, 즉 레지스터의 상태를 말한다.

유지되어야 한다. 예를 들어 아래의 코드 조각을 고려해 보자.

    int a=2;

    ...

    printf("%d\n",a);

    f();

    printf("%d\n",a);

    ...

  위의 예에서 함수 호출 전후에 a를 출력하는 문장은 함수를 호출하기 전에
도 2이고, 함수를 호출하고 나서도 2이다. 이것은 함수 f()를 호출하기 전의
상태와 함수를 호출한 후의 상태가 같다는 것의 간단한 예이다.

  함수 f()를 호출하기 전의 CPU 상태를 S라 하자. 그러면, 함수 f()를 실행하
는 동안 CPU의 동작은 다른 상태 T에 있게 된다. 하지만, 함수 f()를 리턴했
을 때, CPU의 상태는 다시 S가 되어야 한다.

             상태의 보존: 함수의 호출 전에 CPU의 상태는 보존된다. 스택은 상태를
          보존하기 좋은 장소이다. 함수가 리턴되면, 스택에 보존한 기계의 상태를
          복구한다. 함수의 호출 전후의 이러함 작업은 컴파일러에 의해서 자동으로
          생성된다. CPU의 상태를 보존하는 코드를 프리픽스(prefix), 보존된 기계의
          상태를 복구하는 코드를 서픽스(suffix)라 한다. 함수의 호출에서 프리픽스
          와 서픽스의 오버헤드를 어쩔 수 없다.

  함수의 호출 전에 CPU의 레지스터 상태, 플래그 등을 스택에 저장하는 코
드를 실행해야 한다. 이러한 코드는 컴파일러가 자동으로 생성하여 함수의 호
출 앞에 붙이는데 이 코드를 프리픽스(prefix)라 한다. 함수가 리턴하면 저장
된 CPU의 상태를 복구하는 코드를 실행하는데 이 코드 역시 컴파일러가 자
동으로 생성한다. 이 코드를 서픽스(suffix)라 한다.

  그러므로, 함수의 호출에 의해서 발생하는 프리픽스의 실행과 서픽스의 실
행은 어쩔 수 없는 (속도가 느려지는) 오버헤드이다.

  이러한 오버헤드 때문에 간단한 문장은 함수로 구현하지 않고, 문장을 바로
사용하는 것이 속도가 빠르다. 예를 들면 아래의 코드는

...

int i=0;

void Inc() {

    i++;

}

void main() {

    Inc();

    Inc();

}

  i를 증가시키기 위해 함수를 사용하지 않은 아래의 코드보다 속도가 느리
다.

...

int i=0;

void main() {

    ++i;

    ++i;

}

  함수의 이점 - 읽기 쉽다. 문서화(documentation)에 도움이 된다. 유지 보수
(maintenance)가 쉽다 - 을 살리면서, 프리픽스와 서픽스의 오버헤드를 없앨
수는 없을까?

   매크로 함수(macro function)

  C 시절, 그리고 지금(C++시절)도 매크로 함수(macro function)는 여전히
많이 사용한다. 아래의 코드를 보자.

#include <stdio.h>

#define Max(a,b) ((a)>(b)?(a):(b))//매크로 함수를 정의하여 사용한다.

void main() {

    int a=2,b=3;

    printf("%d\n",Max(a,b));

}

  같은 결과를 출력하지만, 매크로 함수를 사용한 위의 소스는 일반 함수를
사용한 이전의 소스보다 조금 빠르다. 왜냐하면, 매크로 함수는 매크로 확장된
다. 즉,

printf("%d\n",Max(a,b));

는

printf("%d\n",((a)>(b)?(a):(b)));

로 매크로 확장(macro extension)되므로, Max()함수의 호출은 일어나지 않
는다. 그러므로, 프리픽스와 서픽스를 실행하지 않으므로, 속도가 조금 빨라지
는 것이다.

  하지만, 매크로 함수를 정의할 때, 우선 순위 문제 등을 조심스럽게 코딩하
여 함수를 정의해야한다. 우리는 매크로 함수의 문제점을 앞 장에서 살펴본
바 있다.

  일반적으로 함수를 정의하듯이 정의하면서(우선 순위 등의 문제를 고려하지
않고), 매크로의 이점(오버헤드가 없음)을 살리는 방법이 C++에 새로이 추가
되었다. 바로 인라인 함수(inline function)를 사용하는 것이다.

   인라인 함수

  인라인 함수를 만들기 위해서, 함수의 선언이나 정의에서 함수의 리턴 형
(return type) 앞에

inline

을 적어준다. inline 키워드가 붙은 함수는 함수의 호출이 일어나지 않는다. 대
신에 컴파일 시간에 인라인 확장(inline extension)된다. 인라인 확장은 매크
로 확장과 거의 비슷하지만, 우선 순위에 대한 문제를 사용자가 고려하지 않
아도 되는 이점이 있다. 또한, 여러 줄로 정의되는 함수를 인라인 함수로 쉽게
설계할 수 있다.

  아래의 예를 참고하자.

#include <stdio.h>

inline int Max(int a,int b) {

    return a>b?a:b;//우선 순위 때문에 걱정할 필요가 없다.

                       //그러므로 이제 괄호 (는 필요가 없다.

}

void main() {

    int a=2,b=3;

    printf("%d\n",Max(a,b));

}

  위의 예에서 Max()는 인라인 함수이다. main()에서 Max(a,b)는 더 이상 함
수 호출이 아니다. 이 문장은 컴파일 시간에 인라인 확장된다. 그러므로, 프리
픽스와 서픽스의 오버헤드는 없다.

  또한 인라인 함수에도 일반 함수처럼 형(type)이 명시됨을 주목하라. 이것이
인라인 확장됨에도 불구하고 컴파일 시간에 엄격한 형 검사(type checking)이
이루어지므로, 매크로 함수의 매크로 확장으로 일어날 수 있는 논리적 에러를
미연에 방지한다. 위의 예에서

printf("%d\n",Max(a,b));

문장은 엄격한 형 검사 이후 아래의 코드로 인라인 확장된다.

printf("%d\n",a>b?:a:b);

   템플릿(template) 인라인 함수

  비록 인라인 함수가 유용하기는 하지만, 매크로 함수의 유연함(인라인 함수
의 강력함에 대응되지만)에 미치지는 못한다. 위에서 정의한 인라인 함수는
오직 정수(integer)에 대해서만 동작한다. 하지만, 매크로 함수

#define Max(a,b) ((a)>(b)?(a):(b))

는 형 자체가 정의된 것이 아니므로, 모든 형에 대해서도 동작한다.

  하지만, 템플릿(template) 인라인 함수를 사용하면, 이러한 걱정은 필요가
없다. 템플릿 인라인 함수는 사용자가 사용한 모든 호출의 형에 대해 템플릿
확장을 한다. 템플릿에 관해서는 '템플릿'을 참고하라.

#include <stdio.h>

template<class T> inline T Max(T a,T b) {

    return a>b?a:b;

}

void main() {

    int a=2,b=3;

    printf("%d\n",Max(a,b));

}

  이제 위에서 정의한 템플릿 인라인 함수는 모든 형 a,b에 대해서도 인라인
확장을 한다. 프리픽스 등의 오버헤드도 없으며, 모든 형에 대해서도 동작하는
유연함을 제공한다.

   자동 인라인(automatic inline)

  클래스의 멤버 함수가 class 구문 안에서 정의된 경우, 이러한 멤버 함수는
자동으로 인라인 함수가 된다. 아래의 예에서 IncAndPrint()는 class 블락 안
에서 정의되었으므로, inline 키워드가 없어도 자동으로 인라인 함수로 간주한
다. 물론 inline 키워드를 명시해도 좋다.

#include <stdio.h>

class CTest {

    int i;

public:

    CTest(){ i=0; }

    void IncAndPrint() {//이 함수는 inline 키워드가 없어도 inline으

                            //로 코드 생성을 시도한다.

        ++i;

        printf("%d\n",i);

    }

};//class CTest

void main() {

    CTest t;

    t.IncAndPrint();

    t.IncAndPrint();

    t.IncAndPrint();

}

  그러므로 위의 소스는 아래의 소스와 동일하다.

#include <stdio.h>

class CTest {

    int i;

public:

    CTest(){ i=0; }

    void IncAndPrint();//void 앞에 Inline을 붙여도 좋다.

};//class CTest

inline void CTest::IncAndPrint() {

    ++i;

    printf("%d\n",i);

}

void main() {

    CTest t;

    t.IncAndPrint();

    t.IncAndPrint();

    t.IncAndPrint();

}

   인라인으로 구현할 수 없는 함수

  인라인 확장에 제어 구문(control structure)인 if,while등의 문장이 들어가면
인라인 확장할 수 있을까? 대개의 경우 그것은 불가능하다. 그러므로, 사용자
가 inline을 명시한 함수에 제어 구문이 들어있으면, 그것은 인라인 함수로 간
주되지 않는다.

#include <stdio.h>

class CTest {

    int i;

public:

    CTest(){ i=0; }

    void IncAndPrint();

    inline void Print10Times() {//이 함수는 제어 구조를 포함하므로 인라

                                //인 함수가 아니다.

        int j;

        for (j=0;j<10;++j) {

            IncAndPrint();

        }

    }

};//class CTest

inline void CTest::IncAndPrint() {

    ++i;

    printf("%d\n",i);

}

void main() {

    CTest t;

    t.IncAndPrint();

    t.IncAndPrint();

    t.IncAndPrint();

}

  프로그램은 실행되지만 컴파일 시간에 경고(warning)가 발생한다.

Warning: Function containing for are not expanded inline

  이 경고는 class 블락 안에서 정의한 Print10Times()에서 발생한 것이다.
Print10Times()의 앞에 붙은 inline을 삭제해도 여전히 경고는 발생한다. class
블락 안에서는 자동으로 인라인 함수로 정의된다는 사실을 잊지 말라.

  어떠한 컴파일러는 인라인 경고를 발생하지 않는다. 또한 코드 최적화(code
optimization)에 의해 inline 키워드 없이도 인라인 함수로 만들기도 한다. 솔
직히 말하면, 현재의 컴파일러 기술에서 인라인은 무의미한 것이다. 조언은 다
음과 같다.

"독자들이 inline의 역할에 대하여 확실히 알고 있다면, inline 키워드를
사용하지 말라. 그것은 무의미한 것이다."

   인라인 배열

  인라인 배열(inline array)에 관한 언급은 '표준화 유감'을 참고하라.

   연습문제

1. 인라인 함수를 사용하지 않고도, 매크로 함수를 사용하여 여러 줄의 함수를
정의할 수 있다. 어떻게 그것을 구현해야 하는가?

2. if,while등이 들어간 문장이 인라인으로 확장할 수 없는 구체적인 예를 들
라.

3.


42. 친구

42. 친구(friends)

  이런 친구를 가지고 싶다.

  나의 모든 것을 말해 놓고도,

  뒷말이 나지 않을까 하고, 걱정하지 않아도 되는 친구.

  나의 모든 것을 공개하고, 주고 싶은 친구.

  그런 친구를 가지고 싶다.

  그렇다면, 걱정이 없다. C++의 친구 함수는 이 모든 것을 제공한다!

  복소수(complex number) 클래스를 만드는 과정을 통해, 친구가 왜 필요한
지에 대해서 알아보자.

#include <stdio.h>

#include <conio.h>

class CComplex {

    float a,b;

  public:

    CComplex(float ta=0.0,float tb=0.0) {

        a=ta;

        b=tb;

    }

    void Add(CComplex c) {

        a+=c.a;

        b+=c.b;

    }

    int IsEqual(CComplex a,CComplex b) {

        return a.a==b.a && a.b==b.b;

    }

};

void main()

{

    CComplex a(1.0,2.0),b(1.0,2.0);

    printf("%d\n",a.IsEqual(a,b));

    //            ^이것이 필요한가?

}

  CComplex 클래스는 두 개의 멤버변수를 가지는데, 이것은 a+bi를 나타낸
다. 생성자는 복소수의 실수부(real part)와 허수부(imaginary part)를 초기화
한다.

  프로그램의 결과는 얼마일까? 1이다. IsEqual()은 파라미터로 두 개의 복소
수를 받아, 이 두개가 같은지를 검사한다. main()안에서 선언된 두 개의 복소
수 객체 a와 b는 각각 1+2i를 의미하므로 같다. 그렇다면, 이 프로그램의 문제
점은 무엇인가? 문제점은 다음과 같다.

"두 개의 복소수를 검사하는 함수가 왜 CComplex의 멤버 함수로 구현되어야
하는가?"

즉,

"왜 IsEqual(a,b)가 아니고, a.IsEqual(a,b)인가?"

이다. IsEqual()은 복소수의 멤버 함수로 구현하는 것 보다, 일반 함수로 구현
하는 것이 바람직하다. 복소수 a와 b를 비교하는 함수가 a의 멤버로 호출될
이유는 없다. 또한 b의 멤버로 호출될 이유도 없다. 그러므로 b.IsEqual(a,b)도
가능하다. 하지만, 어느 것도 설계 개념과 일치하지 않는다. 이제 소스를 수정
해 보자.

#include <stdio.h>

#include <conio.h>

class CComplex {

    float a,b;

  public:

    CComplex(float ta=0.0,float tb=0.0) {

        a=ta;

        b=tb;

    }

    void Add(CComplex c) {

        a+=c.a;

        b+=c.b;

    }

};

int IsEqual(CComplex a,CComplex b) {

    return a.a==b.a && a.b==b.b;

    //       ^privacy principle을 위배하고 있다.

}

void main()

{

    CComplex a(1.0,2.0),b(1.0,2.0);

    printf("%d\n",IsEqual(a,b));

}

  위 프로그램은 문제점이 또한 존재한다. 이번에는 아예 컴파일 조차 되지
않는다. 왜 그런가? 그것은 IsEqual()이 CComplex의 private멤버 a와 b를 접
근(access)하기 때문이다1). 그렇다면 방법이 없다는 말인가? 아니다. 바로 친
구함

  1) 프라이버시의 원리(privacy principle)에 의하면, private 멤버는 아무도 접근할 수 없
다.

    하지만, 친구(friend) - 친구 함수, 친구 클래스 - 만은 접근할 수 있다.

수를 이용하는 것이다.

  CComplex가 IsEqual()을 친구 함수로 선언하는 것이다. IsEqual()이
CComplex의 친구로 선언되면, IsEqual()은 CComplex의 모든 멤버에 대해 접
근이 가능하다. 수정된 소스는 아래와 같다.

#include <stdio.h>

#include <conio.h>

class CComplex {

    float a,b;

  public:

    CComplex(float ta=0.0,float tb=0.0) {

        a=ta;

        b=tb;

    }

    void Add(CComplex c) {

        a+=c.a;

        b+=c.b;

    }

    friend int IsEqual(CComplex a,CComplex b);

};

int IsEqual(CComplex a,CComplex b) {

    return a.a==b.a && a.b==b.b;

}

void main()

{

    CComplex a(1.0,2.0),b(1.0,2.0);

    printf("%d\n",IsEqual(a,b));

}

  위 소스는 제대로 컴파일되며, 1을 출력한다.

  친구 함수를 쓸 때, 주의할 점은 많다. 다음과 같은 사항들을 주의하라.

  (1) CComplex가 IsEqual()을 친구로 선언한 것이지, IsEqual()이 CComplex
의 친구가 된 것은 아니다. 그러므로, IsEqual()의 선언은 CComplex안에 위치
해야 하며, IsEqual()이 CComplex의 모든 멤버를 접근할 수 있다. 즉,
IsEqual()에서 CComplex를 친구로 선언해서는 안 되며, CComplex가
IsEqual()의 멤버 - 지역 변수 등 - 를 접근할 수 있는 것은 아니다.

  이러한 원리는 실세계(real world)에서도 존재한다. A가 B를 친구로 생각해
서 자신의 모든 것을 공개하고 보여주지만, A는 B의 모든 것을 볼 수 있는
것은 아니다. 즉 A가 "B, 너는 나의 친구야." 라고 말하면, B만이 A의 모든
것을 접근할 수 있는 것이다. 또한 A가 B를 친구로 선언한 것이다.

  (2) 친구 함수는 멤버 함수가 아니다. 그러므로 친구 함수 앞에는 범위 해
결사가 붙으면 안된다. 즉 위의 소스를 다음과 같이 고치면 오류(error)다.

    int CComplex::IsEqual(CComplex a,CComplex b) {

        return a.a==b.a && a.b==b.b;

    }

  (3) 일반 함수가 친구가 되는 것이다. 즉 IsEqual()이란 일반 함수를 친구로
선언한 것이기 때문에, IsEqual()을 정의하는 쪽에서는 friend란 키워드를 사
용하면 안된다. 즉 다음 문장 역시 오류이다.

    friend int IsEqual(CComplex a,CComplex b) {

        return a.a==b.a && a.b==b.b;

    }

  (4) 친구의 친구는 친구가 아니다. 이 규칙은 친구 규칙(friendship
principle)이라고 알려져 있다. 지금은 적절한 예를 들 수 없지만, 'A가 B의
친구이고, B가 C의 친구라고 해서, A가 C의 친구는 아니다'라는 의미이다. 이
것은 친구 클래스를 할 때 자세히 다룬다.

  또한 친구 함수를 사용하는 권장 사항은 다음과 같다.

"친구 함수의 사용은 클래스의 추상화(abstraction)를 위반하는 것이다. 그
러므로 빈번한 친구 함수의 사용은 권장되지 않는다. 오직 필요할 때만, 친구
함수를 사용하라."

  또한, 반드시 친구 함수로만 구현해야 하는 함수가 존재한다는 사실을 알고
있어야 한다. 이것은 '연산자 오버로딩'에서 자세히 다룬다.

  친구 클래스: friendship principle

  실습문제

1.


43. 가상함수

43. 가상 함수(virtual
function)

  가상(virtual)의 의미

  가상의 사전적인 의미는 '실제로 존재하지 않는' 이다. 이 의미는 C++에서
함수나 클래스가 실제로 존재하지 않는다는 것■을 나타내기 위해 쓰인다.

            엄격히 말하면, 함수나 클래스의 코드가 실제로 존재하지 않는 다는 것이
          아니라, 함수의 호출과 클래스의 참조가 컴파일 시간(compile time)에 정확
          히 알려지지 않는다는 의미이다. 가상 함수나 가상 클래스는 실행 시간(run
          time)에 정확히 결정된다. 함수나 변수의 주소가 결정되는 것을 바인딩
          (binging)이라고 하는데, 컴파일 시간에 바인딩이 되면, 이른 바인딩(early
          binding), 실행 시간에 바인딩이 되면, 늦은 바인딩(late binding)이라고 한다.
          virtual이 붙으면 모두 늦은 바인딩이 된다. 그래서 가상(virtual)이란 용어를
          사용한다. 독자들인 가상 함수 혹은 가상 베이스 클래스의 개념을 잊어버
          리지 않기 위해 늦은 바인딩, 즉 실행 시간 바인딩을 항상 기억하라.

virtual 키워드는 멤버 함수 선언의 앞에, 클래스의 다중 상속(multiple
inheritance)에서 베이스 클래스 이름 앞에 사용되어 바인딩이 실행 시간에
이루어져야 함을 컴파일러에게 알려 준다.

  virtual과 같이 선언된 멤버 함수는 가상 멤버 함수(virtual member
function), 간단히 가상 함수(virtual function)라고 하며, 컴파일 시간에 함수
의 주소가 결정되지 않는다. 또한 클래스의 다중 상속에서 virtual을 사용하여
상속받으며, virtual이 명시된 클래스는 가상 베이스 클래스(virtual base
class)가 된다. 가상 베이스 클래스를 하나 이상 가지는 유도 클래스
(derived class)의 객체가 만들어 질 때, 클래스 구조 정보(class hierarchy
information)는 실행 시에 동적으로 유지■된다.

            동적으로 유지되는 정보를 이용하여, 두 개 이상의 베이스 클래스에서 유
          도된 유도 클래스의 객체가 만들어 질 때, 일어날 수 있는 충돌 문제를 해
          결할 수 있다. 이러한 충돌 해결은 컴파일 시간에는 불가능하다.

  subtype의 원리(subtype principle): 아버지
는 자식을 가리킬(point) 수 있어도, 자식은 아버지를
가리킬 수 없다.

  가상 함수를 논하기 전에 먼저 살펴보아야 하는 것은 'subtype의 원리'이
다. 이 원리는

"원래의 타입(original type)이 있어야 하는 자리에는, 그 타입의
서브타입(subtype)이 올 수 있다."

는 원리이다. 이 원리는 아주 중요■하며, 중요한 응용을 가진다. 먼저 아래의

            하지만, 최근의 ANSI C++에서는 static_cast, dynamic_cast등을 이용하여 이
          원리를 무시하는 것이 가능하다. '새로운 형태의 형변환'을 참조하라.

예제를 살펴보자. 이 예제는 아래 그림의 계층 구조를 가지는 클래스를 구현
한 것이다.

             B는 A를 상속 받는다. C는 B를 상속 받는다. 프로그래머는 A 클래스의
          포인터로 B 혹은 C 클래스 객체의 멤버 함수를 호출할 생각이다. 이러한
          응용은 빈번하며 중요하다.

#include <stdio.h>

#include <iostream.h>

class A {

    int i;

public:

    A() {

        i=0;

    }

    void Print() { cout << i << endl; }

};//class A

class B : public A {

    int i;

public:

    B() {

        i=1;

    }

    void Print() { cout << i << endl; }

};//class B

class C : public B {

    int i;

public:

    C() {

        i=2;

    }

    void Print() { cout << i << endl; }

};//class C

void main() {

    A* ap;

    B b;

    C c;

    ap=&b;//이 문장이 가능한가?

    ap->Print();//가능하다면, 무엇이 호출되는가?

    ap=&c;

    ap->Print();

}//main

  독자들은 위 프로그램의 출력결과를 예측하였는가 결과는 아래와 같다.

0

0

이 결과가 다소 의외일는지 모른다. 하지만, 이것은 이제 설명할 가상 기능을
위한 적절한 예이다. 위의 예에서 우리가 살펴보아야 할 사항은 2가지이다.

    ■ A클래스의 포인터에 B클래스의 포인터를 대입하는 것이 가능한

    가? 즉 아래의 문장은 에러가 아닌가?

    ...

    ap=&b;

    ...

      위의 문장은 에러가 아니다.

    ■ 위의 문장이 가능하다면, ap->Print()는 A의 멤버를 호출하는가?

    B의 멤버를 호출하는가?

      A의 멤버 함수 Print()를 호출한다.

  위의 2가지 상황에 대한 설명이 매우 불만인 독자는 가상 기능을 이용해 이
문제를 해결할 수 있으므로, 걱정하지 말라. 먼저, 첫 번째 사항은 그 유명한
'서브타입의 원리(subtype principle)'이다.

  할당문의 왼쪽이 A형이라면, 할당문의 오른쪽은 당연히 A형이 와야 한다.
예를 들어,

    int i=1,j=2;

    char c=3;

과 같은 변수 선언에서

i=c;

는 엄격한 의미에서 잘못이다. 명시적인 형 변환(type conversion)을 해서

i=(int)c;

같은 대입문을 사용하는 것이 바람직하다.

  이 문제를 클래스에 적용해 보자. 왼쪽에 A*형이 왔을 때, 오른쪽에는 A*
형이 와야 한다. 또한 서브 타입의 원리에 의해, 오른쪽에는 A보다 아래 형
(subtype)의 주소가 오는 것이 가능하다. 즉,

"그 형이 쓰여야 할 자리에는 그 형의 서브 타입이 올 수 있다."

는 것이다. 그러므로 아래 문장은 컴파일 시간 에러이다.

    A a;

    B* bp;

    bp=&a;

왼쪽이 B*이므로 오른쪽에는 B* 혹은 C*는 올 수 있어도, A는 B의 상위 타
입(supertype)이기 때문에 A*는 올 수 없다■.

            이것은 우리가 일반적으로 생각하는 - 비록 그것이 틀리기는 하지만 - 개
          념과 일치한다. 자동차(CCar)를 상속받은 띠뷰롱(CSportsCar)이 있다고 하
          자. 비록 띠뷰롱이 자동차에 비해 구체화된 많은 정보를 가지고는 있지만,
          띠뷰롱의 포인터에 자동차의 포인터를 대입하는 것은 불가능하다. 아버지
          는 자식을 가리킬 수 있어도, 자식은 아버지를 가리킬 수 없다.

  두 번째 사항은 다소 불만이다. 비록 포인터가 A*이지만, 대입된 포인터가
B*였다면, B의 멤버 함수 Print()가 호출되는 것이 자연스럽다. 하지만, 이러
한 정보를 컴파일 시간에 모두 아는 것은 큰 오버헤드(overhead)이다. 컴파일
러는 ap->Print()의 바인딩을 위해 단지 ap만을 참조■한다. ap는 A*이므로

            ap는 심벌 테이블(symbol table)에 정보가 저장되어 있다. 컴파일러는 함
          수 주소를 얻기 위해 단지 심벌 테이블만을 참조한다.

Print()는 A의 멤버가 호출된다.

  이것이 B*라고 알려줄 수 는 없는가? 그렇게 하기 위해서는 컴파일 시간에
클래스의 계층 구조를 모두 추적해 보아야 할 것이다. 이것이 불가능한 것은
아니지만, C++은 더 좋은 해결책을 제시하였다. 바로 실행 시에 멤버 함수의
주소를 결정하는 것이다. 그렇게 하려면, 함수의 헤더 앞에 virtual을 붙여서
이 함수의 바인딩이 실행 시에 일어나야 할 것임을 알려주어야 한다.

  수정된 소스와 결과는 아래와 같다. 수정된 부분은 단지 멤버 함수 Print()
에 virtual을 붙인 것뿐이다.

#include <stdio.h>

#include <iostream.h>

class A {

    int i;

public:

    A() {

        i=0;

    }

    virtual void Print() { cout << i << endl; }

};//class A

class B : public A {

    int i;

public:

    B() {

        i=1;

    }

    virtual■ void Print() { cout << i << endl; }

            virtual 키워드는 베이스 클래스에서 한 번만 지정하면 된다. 그러므로 여기
          서나 아래 클래스 C에서 명시적으로 virtual을 적을 필요는 없다. 하지만,
          필자는 이 함수가 virtual인 것을 명확히 하기 위해 - 문서화에 도움이 된
          다 - 모두 virtual을 붙여 쓰는 것을 좋아한다.

};//class B

class C : public B {

    int i;

public:

    C() {

        i=2;

    }

    virtual void Print() { cout << i << endl; }

};//class C

void main() {

    A* ap;

    B b;

    C c;

    ap=&b;

    ap->Print();

    ap=&c;

    ap->Print();

}//main

결과는

1

2

이다.

  실행 결과 외에 차이가 있다면, 조금 늦어진 속도이다. 실행시간에 클래스
계층 구조를 살펴보아야 하므로 멤버 함수 호출이 조금 늦어진 것이다(물론
느끼지는 못하지만).

  실제로 가상 함수의 주소를 매 객체마다 어떻게 유지하여 제대로 함수를 호
출하는가는 큰 의문이다. 이것은 우리가 생각하는 것처럼 조잡하지는 않다. 이
주제는 이 장의 마지막에서 가상 함수 테이블(virtual table: v-table)에서
다루기로 하고, 넘어가도록 하자.

  지금까지의 내용을 한 마디로 요약하면 다음과 같다.

"베이스 클래스의 포인터를 이용하여 하위 클래스(derived class)의 멤버
함수를 호출하기 위해서는, 멤버 함수가 가상이어야 한다. 즉, 하위
클래스에서 오버라이딩(overriding)될 것 같은 함수는 베이스에서 모두
가상으로 선언하라."

  하나의 인터페이스(interface)에 다수의 방법
들, 가상 함수, 추상 클래스(abstract class), 실행 시
다형성(runtime polymorphism)

  가상 함수가 왜 유용한가? 이것은 '하나의 인터페이스에 다수의 방법들■'

            C++의 창시자인 Bjarne Stroustrup이 그의 저서에서 이 문장을 사용하고 있
          다.

이란 문장이 잘 표현한다. 위의 예에서 같은 이름의 Print() 멤버 함수는 다른
일을 한다.

  순수 가상 함수(pure virtual function)

  독자들은 베이스 클래스에서 멤버 함수를 만들 때, 어떤 함수들을 가상으로
선언해야 할 것인가를 알고 있을 것이다. 어떤 클래스는 순전히 상속을 위한
목적으로 만들어지는데, 이러한 클래스는 반드시 베이스 클래스로만 사용될
것이다. 이러한 클래스의 멤버 함수는 다음과 같은 3부류의 멤버 함수가 있을
수 있다.

    ■ 베이스 클래스에만 필요한 함수

    ■ 베이스에서 일부 기능을 정의하고, 상속된 클래스에서 나머지 기

    능이 정의되어야 하는 함수

    ■ 반드시 필요하지만, 상속된 클래스에서만 정의 가능한 함수

  베이스에서 일부 기능을 정의하고, 나머지 기능을 상속된 클래스에서 구현
해야 하는 경우는 C++의 범위 해결사(scope resolver)를 사용하여 구현할
수 있으며, 이러한 구현은 일반적이며 매우 중요한 응용 분야를 가지고 있다.

  유도 클래스 B에서 베이스 클래스 A의 Print()를 호출하기 위해서는

A::Print();

처럼 사용한다■. A가 클래스 이름이기 때문에

            물론 Print()는 A의 공개(public) 멤버여야 한다.

A.Print();

처럼 사용할 수는 없다. 또한 객체가 만들어진 시점이 아니라, 클래스를 정의
하는 시점이기 때문에 A::Print() 처럼 사용하는 것은 타당(valid)하다. 아래의
예제를 참고하라.

#include <stdio.h>

#include <iostream.h>

class A {

public:

    virtual void Print() {

        cout << "a job in Base" << endl;

    }

};//class A

class B : public A {

    int i;

public:

    B() {

        i=1;

    }

    virtual void Print() {

        A::Print();//베이스 클래스의 멤버 함수를 호출하기 위해서는

                    //범위 해결사(::)를 사용한다.

        cout << "a job in Derived" << endl;

        cout << i << endl;

    }

};//class B

void main() {

    A* ap;

    B b;

    ap=&b;

    ap->Print();

}//main

결과는

    a job in Base

    a job in Derived

    1

이다.

  위의 예에서 굵은 문자 부분을 주목하라. 유도 클래스에서 베이스 클래스의
멤버 함수를 호출하는 방법은 클래스 이름(A)과 범위 해결사(::)를 사용하는
것이다.

  세 번째 경우, 즉 함수가 반드시 필요하지만, 기능은 유도 클래스에서 정의
되어야 하는 경우는 어떻게 구현할 것인가? 독자들은 옛 C 스타일의 주석을
사용할지도 모른다.

...

class A {

public:

    //virtual void Print();

    //반드시 읽어야 할 주석: Print()함수는 유도 클래스에서 반드시 정의

    //되어야 함, 그렇지 않으면 이 클래스는 폭발함

};//class A

...

  어떤 독자들은 아마도 몸체만 있고 내용은 없는 빈 함수를 만들어 놓을지도
모른다.

...

class A {

public:

    virtual void Print() {

        //반드시 읽어야 할 주석: Print()함수는 유도 클래스에서 반드시 정

        //의 되어야 함, 그렇지 않으면 이 함수는 폭발함

    }

};//class A

...

  실제로 이러한 방법은 C++의 순수 가상 함수(pure virtual function)가
나오기 전에 사용되었다. 하지만, C++ 프로그래머는 아래와 같이 이 함수가
순수 가상(pure virtual)이라는 것을 알려주어 구현한다.

...

class A {

public:

    virtual void Print()=0;

};//class A

...

  더 이상 주석은 필요 없다. C++의 문법 자체가 주석을 대신한다. 독자들은
함수 끝에 있는

=0

의 황당한 대입문에 놀라지 말라. 이것은 함수를 0으로 초기화하는 문장이 아
니다. 단순히 문법 그 자체이다. 이것은 이 함수가 유도 클래스에서 재정의 되
어야 하는 순수 가상 함수임을 알려준다. 아래의 예제를 참고하라.

#include <stdio.h>

#include <iostream.h>

class A {

public:

    virtual void Print()=0;//이것은 대입문이 아니라, 순수 가상 임을 나

                           //타낸다.

};//class A

class B : public A {

    int i;

public:

    B() {

        i=1;

    }

    virtual void Print() {

        cout << "a job in Derived only" << endl;

        cout << i << endl;

    }

};//class B

void main() {

    A* ap;

    B b;

    ap=&b;

    ap->Print();

}//main

결과는

    a job in Derived only

    1

이다.

  순수 가상 함수가 사용되었을 때, 이 클래스의 객체를 만드는 것은 당연히
불가능하다. 정의되지 않은 함수를 가지는 객체를 어떻게 만들 수 있는가? 이
처럼, 최소한 1개 이상의 순수 가상 함수를 가지는 클래스는 다른 클래스의
베이스 클래스로만 사용되며, 이런 클래스를 추상 클래스(abstract class)라
한다■. 즉 추상 클래스는 '객체를 만들 수 없는 클래스', '순수 가상 함수를

            추상 클래스가 아닌 클래스는 객체를 만들 수 있으며, 구체 클래스
          (concrete class)란 말이 적당한 것 같다. 하지만, 이런 용어는 사용하지
          않는다.

가지는 클래스' 이다.

  가상 베이스 클래스(virtual base class)

  다중 상속에 의해 발생할 수 있는 문제점 중의 하나는 공통된 조상
(common ancestor)으로부터 유도되는 다른 경로(path)의 자손 클래스 때문에,
유도 클래스의 객체가 만들어 질 때, 여러 번 참조되는 베이스 클래스 때문이
다. 아래 그림을 참고하자.

            유도 클래스 D에서 조상 A로의 경로는 B를 통해, 혹은 C를 통해 2가지가
          가능하다. 이러한 정보를 컴파일 시간에 알 수 없는 것은 아니지만, 오버
          헤드가 된다. 실행 시간에 이러한 정보를 이용하여 클래스 A는 한 번만 참
          조되도록 해야 할 것이다.

#include <stdio.h>

#include <iostream.h>

class A {

public:

    A() { cout << "constructing A" << endl; }

    ~A() { cout << "destructing A" << endl; }

};//class A

class B : public A {

public:

    B() { cout << "constructing B" << endl; }

    ~B() { cout << "destructing B" << endl; }

};//class B

class C : public A {

public:

    C() { cout << "constructing C" << endl; }

    ~C() { cout << "destructing C" << endl; }

};//class C

class D : public B, public C {

public:

    D() { cout << "constructing D" << endl; }

    ~D() { cout << "destructing D" << endl; }

};//class D

void main() {

    D d;

}//main

결과는

    constructing A

    constructing B

    constructing A

    constructing C

    constructing D

    destructing D

    destructing C

    destructing A

    destructing B

    destructing A

이다. 위의 결과가 예상대로였는가■? A를 참조하는 경로가 2개이므로, A의

            생성자는 베이스 클래스부터 호출된다. 소멸자는 유도 클래스부터 먼저 호
          출된다. 부모가 먼저 있어야 자식이 있을 수 있으므로, 부모 클래스의 생
          성자가 먼저 호출되는 것은 자연스럽다. 하지만, 소멸자의 호출은 반대이
          다. 자식 클래스는 부모 클래스를 특성화(specification)하므로, 세부적인 사
          항이 먼저 사라지고, 일반적인 사항이 사라져야 한다.

생성자와 소멸자(destructor)는 2번 호출되었다. 이것은 사소한 혹은 심각한
에러를 유발한다. 각각은 모호함(ambiguity)과 메모리 샘(memory leak)이
다.

모호함

  위의 경우 생성자의 호출은 A, B, A, C의 순이었다. 왜 C보다 B의 생성자
가 먼저 호출되는가? 이것은 클래스 D를 정의할 때, 베이스 클래스를 명시한
순서 때문이다.

    class D : public B, public C {

    ...

  만약 위의 정의를 아래처럼 바꾼다면, A, C, A, B의 순으로 생성자가 호출
될 것이다.

    class D : public C, public B {

    ...

  이러한 사실은 못마땅하다. 베이스 클래스로 B와 C를 모두 이용하는 2개
이상의 클래스를 만들 경우, 경로에 따른 약간의 미묘한 문제가 발생한다. 위
의 경우 B와 C의 순서가 알려져 있지 않으므로, 클래스를 E를 정의하기 위해
아래의 문장을 사용했다고 하자.

    class E : public C, public B {

    ...

  D는 A,B를 호출하고, A,C를 호출하지만, E는 A,C를 호출하고, A,B를 호출
한다. 대부분의 경우 차이가 없겠지만, A,B → A,C의 호출과 A,C → A,B의
호출은 분명히 다르며, 모호하다■.

            이것은 B → C의 호출과 C → B의 호출과는 구분되어야 한다. 이것의 호
          출 순서를 구분 짓는 유일한 문법은 명시한 순서이다. 만약 B→C의 호출
          과 C→B의 호출의 결과가 다르다면, 독자들은 조심스럽게 베이스 클래스
          생성자의 호출 순서를 유지해야 한다. 하지만, 여기서의 문제는 A→C의 호
          출이 먼저냐, A→B의 호출이 먼저냐의 문제이다. B와 C의 순서에는 상관
          없이 A와 연관된 순서의 문제는 거의 없지만, 최소한 있을 수 있다..

메모리 샘■

  보통 메모리 누수(漏水)라고 번역한다.

  상속받는 2개 이상의 경로가 존재하는 경우, 심각한 문제는 메모리 샘
(memory leak) 문제이다.

  만약 A의 생성자에서 동적으로 메모리를 할당한다면, A의 생성자는 2번 호
출되므로, 첫 번째 포인터는 댕글거리게(dangling) 된다. 또한 A의 소멸자가
비록 2번 호출되더라도, 포인터가 2개 유지되는 것이 아니므로, 타당한 소멸이
일어나지 않는다. 이 문제는 심각하다. 아래의 그림을 보라!

             A→B의 경로를 통해 처음 A의 생성자가 호출되며, 이 때 메모리 할당이
          일어나서, 변수 pointer는 할당된 메모리를 가리킨다. A→C의 경로를 통해
          2번째의 생성자가 호출되며, pointer는 새로운 메모리 블락을 가리킨다. 처
          음 메모리 블락을 가리키는 포인터는 사라진다.

             이제 소멸자의 호출을 고려해 보자. C→A의 호출을 통해, mem2가 해제된
          다. B→A의 호출이 mem1을 해제할 것을 기대하지만, 이미 그 포인터는 무
          효(invalid)하다.

             mem1을 가리키는 포인터는 어디에도 없으므로, 메모리는 샌다.

  위의 설명이 맞는가? 이 부분의 설명이 어딘가 이상한 독자는 연습문제 1
을 참고하라.

  이 문제 - 모호함 - 에 대한 타당한 해결책은 베이스 클래스의 생성자가 1
번만 호출되게 하는 것이다. 이것은 실행 시간에 클래스의 구조 정보(class
hierarchy information)를 이용함으로써 가능하다. 즉, 베이스 클래스 생성자
호출에 관한 정보가 실행 시에 결정되어야 한다. 실행 시에 바인딩이 일어나
게 하기 위해, 어떤 키워드를 사용했는지 기억나는가? 바로 virtual 이다.

  중복되게 클래스를 상속할 때는 항상 virtual을 사용하라. 이것은 베이스 클
래스를 가상으로 만든다. 위의 예에서는 A가 중복되므로, A를 가상 베이스
클래스(virtual base class)로 선언해야 한다. 베이스 클래스를 가상으로 만
드는 방법은 유도 클래스에서 베이스 클래스를 상속할 때 접근 변경자
(access modifier■) 앞에 virtual을 붙이는 것이다. 만약 B 클래스가 A를 공

            private, public과 protected는 베이스 클래스의 멤버를 접근하는 방식을 제
          한한다. 이것이 클래스 안에서 멤버의 접근 속성을 제한하기 위해 쓰였을
          때는 접근 지정자(access qualifier)라고 한다. 하지만, 상속에서는 이미 지
          정된 클래스의 접근 속성을 변경(modify)하기 위해 사용되므로, 접근 변경
          자(access modifier)라 한다. 하지만, 엄격한 구분이 그리 중요한 것은 아
          니다.

개적으로 상속받으며 A를 가상 베이스 클래스로 만들기 위해서는

    class B : virtual public A {

    ...

와 같이 선언한다. 아래의 예제와 실행 결과를 참조하라.

#include <stdio.h>

#include <iostream.h>

class A {

public:

    A() { cout << "constructing A" << endl; }

    ~A() { cout << "destructing A" << endl; }

};//class A

class B : virtual public A {

public:

    B() { cout << "constructing B" << endl; }

    ~B() { cout << "destructing B" << endl; }

};//class B

class C : virtual public A {

public:

    C() { cout << "constructing C" << endl; }

    ~C() { cout << "destructing C" << endl; }

};//class C

class D : public B, public C {

public:

    D() { cout << "constructing D" << endl; }

    ~D() { cout << "destructing D" << endl; }

};//class D

void main() {

    D d;

}//main

실행 결과는

    constructing A

    constructing B

    constructing C

    constructing D

    destructing D

    destructing C

    destructing B

    destructing A

이다.

  가상 소멸자(virtual destructor)

  생성자는 상위 클래스에서 하위 클래스의 순서로 호출된다. 한 조상을 여러
개의 경로를 통하여 상속받는 경우, 한 부모 클래스의 생성자가 여러 번 호출
되는 것을 피하기 위하여, 클래스를 가상으로 만드는 것에 대해서 배웠다.

  소멸자는 하위 클래스에서 상위 클래스의 순서로 호출된다. 하지만, 상위 클
래스의 포인터가 동적으로 할당된 하위 클래스의 객체를 가리킬 때, 하위 클
래스의 소멸자는 호출되지 않는다. 컴파일 시간(compile time)에 상위 클래스
의 포인터가 하위 클래스를 가리키고 있다는 것을 어떻게 알 수 있는가? 그
러므로 이러한 소멸자의 호출이 실행 시간(run time)에 일어나도록 해야 한
다. 그렇다. 소멸자를 가상으로 선언해야 한다.

  아래의 예제를 참고하라. 만약 B의 상위 클래스 A에서 소멸자를 virtual로
선언하지 않으면, 어떻게 출력될지를 예상해 보라. 그리고 실행해 보라. 또한
하위 클래스 B에서는 소멸자에 virtual을 붙이지 않아도 됨에 불구하고, 가상
소멸자임을 명확히 하기 위해, virtual을 붙였음에 주목하라.

#include <iostream.h>

#include <stdio.h>

#include <string.h>

class A {

    char* a;

public:

    A(char* s) {

        a=new char[strlen(s)+1];

        strcpy(a,s);

    }

    virtual ~A() {

        delete[] a;

        printf("Destructor of class A\n");

    }

    virtual void Print() {

        printf("%s\n",a);

    }

};

class B : public A {

    char* b;

public:

    B(char* s,char* t) : A(t) {

        b=new char[strlen(s)+1];

        strcpy(b,s);

    }

    virtual ~B() {

        delete[] b;

        printf("Destructor of class B\n");

    }

    virtual void Print() {

        A::Print();

        printf("%s\n",b);

    }

};

void main() {

    A* pA;

    pA=new B("hello","world");

    pA->Print();

    delete pA;

}

  실행 결과는 아래와 같다.

    world

    hello

    Destructor of class B

    Destructor of class A

  위의 경우 소멸자를 가상으로 선언하지 않으면 문제는 심각하다. 생성자에
서 동적으로 메모리를 할당하고 있기 때문에, 소멸자가 가상으로 선언되지 않
으면, 메모리 샘(leak)이 발생할 것이다. 규칙은 다음과 같다.

"상위 클래스로 사용될 것 같은 클래스의 생성자에서 동적으로 메모리를
할당하는 경우, 소멸자를 가상으로 선언하라."

  메시지 맵(message map)

  가상 함수를 가장 적절히 잘 활용한 예는 마이크로소프트 윈도우용 표준
C++ 라이브러리에서 찾아 볼 수 있다. 볼런드■의 OWL(Object Window
Library)과 마이크로소프트의 MFC(Microsoft Foundation Class)등이 적절한
예이다.

            오늘(1998년 6월) 볼런드가 회사 이름을 바꾸었다. Integrating the Enterprise
          란 의미의 INPRISE로 이름을 바꾼 것이다. 기업을 통합하는데 해결책을
          제시한다는 의미를 강조한 것 같다. 웹 사이트는 www.borland.com에서
          www.inprise.com으로 바뀌었다. 바뀐 회사의 로고는 다음과 같다.

                   이 책은 차례대로 쓰인 것이 아니기 때문에 이 책에 사용된 볼런드란 이
          름을 모두 인프라이즈로 바꾸는 것은 무리인 것 같다. 또한 갑작스런 결정
          은 아닌 것 같으며, 볼런드가 자사의 최신 컴파일러 'C++ 빌더 3.0'을 올
          해(1998년) 발표할 때 여전히 '볼런드 C++ 빌더'란 이름을 사용한 것으로
          보아, 제품군에는 여전히 '볼런드'란 이름을 사용할 지는 의문이다.

여기서는 MFC의 메시지 맵 구조(message map architecture)가 어떻게 가
상 함수를 이용하는지에 대해서만 설명하겠다.

             MFC의 메시지 맵 구조: 베이스 클래스에서 유도 클래스의 이벤트 핸들러
          (event handler) 함수를 호출하기 위해, 메시지의 이름과 함수의 주소를 가
          지는 테이블을 참조하는데 이 테이블을 메시지 맵(message map)이라 한
          다. 이 테이블에 메시지 타입과 이벤트 핸들러의 주소를 대입하는 코드를
          생성하는 부분은 클래스 마법사(class wizard)에 의해 자동화되어 있다.
          일부 윈도우 이벤트에 대해서는 가상 함수를 이용하여 구현되어 있지만,
          속도 문제 때문에 대부분의 이벤트에 대해서는 메시지 맵 테이블로 구현
          된다.

  아래와 같은 클래스의 계층 구조를 고려해 보자.

             CObject의 OnDraw() 함수는 가상으로 선언되어 있다. 그러므로 CWnd의
          OnDraw() 역시 가상이다. CWnd를 상속 받은 클래스에서 OnDraw()를 만들
          때, 베이스 클래스의 OnDraw()를 불러 주고, 사용자가 자신이 해야 할 일
          을 추가하는 부분을 자동화하는 것이 가능하다. 이것을 위해 약간의 기교
          (trick)가 필요하다.

  윈도우는 메시지 방식으로 동작하며, 각 응용 프로그램은 메시지를 받기 위
해 메시지 큐(message queue)와 메시지 루프(message loop)를 가진다. 물론
메시지 큐는 운영체제에 의해서 유지되지만, 메시지 루프는 프로그래머가 직
접 코딩해 - 일반적으로 WinMain()안에 - 주어야 한다.

  예를 들어, 응용 프로그램 위에서 마우스 왼쪽 버튼이 눌렸다고 하자. 윈도
우는 응용 프로그램의 메시지 큐에 WM_LBUTTONDOWN■메시지와 적절한

            WM은 Window Message의 약자이다.

파라미터를 메시지 큐에 넣는다. 응용 프로그램의 WinMain()■에 설치된 메시

            윈도우용 C/C++ 프로그램은 main()이 아닌 WinMain()에서 시작한다.

지 루프는 자신의 메시지 큐를 검사하다가 새로운 메시지
WM_LBUTTONDOWN이 큐에 들어온 것을 알고 이 메시지를 꺼내온다. 그
리고 메시지를 처리하는 함수인 WinProc()■을 - 자신이 꺼내온 메시지를 파

            항상 이름이 WinProc()인 것은 아니다. 윈도우 프로시저(window
          procedure)로 불리는 이 특별한 함수는 WinMain()에서 윈도우 클래스를 등
          록할 때 파라미터로 설정해야 하며, 일반적으로 WinProc()이란 이름을 사용
          한다.

라미터로 - 불러달라고 윈도우에게 요청 - 디스패치(dispatch)라 한다 - 한다
(윈도우 프로그램에서 어떤 함수들은 종종 불러달라고 요청해야한다. 즉 사용
자가 직접 호출할 수 없다. 윈도우 프로시저가 대표적인 경우인데 이러한 함
수를 콜 백 함수(call back function)라 한다). 응용 프로그램의 요청을 받은
윈도우는 WM_LBUTTONDOWN과 적절한 파라미터 - 이 경우 마우스 버튼
의 상태와 커서의 위치 - 로 응용 프로그램의 메시지 처리 함수 WinProc()을
호출한다.

  WinProc()은 물론 switch문을 사용하여 WM_LBUTTONDOWN등의 메시
지를 검사하여, 응용 프로그램에 적절한 처리를 수행할 것이다. 그러므로 윈도
우 프로그래밍을 할 때, WinMain()의 메시지 루프와 윈도우 프로시저는 반드
시 필요한 부분이다.

  OWL과 MFC는 C++의 클래스를 이용하여, 메시지 루프와 윈도우 프로시져
를 자동화했다. WinMain()과 WinMain()의 메시지 루프는 상위 클래스에 숨
겨져 있으며, 윈도우의 메시지를 처리하는 윈도우 프로시져 역시 상위 클래스
에 숨겨져 있다.

  아래의 예제는 베이스 클래스에서 정의된 가상 함수를 상속받은 클래스에서
어떻게 사용하는지를 잘 보여준다.

#include <iostream.h>

class CObject {

public:

    virtual void OnDraw() {

        cout << "OnDraw() of CObject" << endl;

    }

};//class CObject

class CWnd : public CObject {

public:

    virtual void OnDraw() {

        CObject::OnDraw();

        cout << "OnDraw() of CWnd" << endl;

    }

};//class CWnd

class CView : public CWnd {

public:

    virtual void OnDraw() {

        CWnd::OnDraw();

        cout << "OnDraw() of CView" << endl;

    }

};//class CView

CObject *obj;

CView view;

void main() {

    obj=&view;

    obj->OnDraw();

}//main

실행 결과는

    OnDraw() of CObject

    OnDraw() of CWnd

    OnDraw() of CView

이다.

  가상 함수를 이용하면, 특정한 부분의 소스 코드를 생성하는 것을 자동화할
수 있다. MFC⼆ 라이브러리의 메시지 맵(message map)은 이의 훌륭한 예

            Microsoft Foundation Class 라이브러리로 Visual C++에서 Win95용 응용 프로그램을
          개발하기 위해서 제공하는 C++ 라이브러리이다.

이다. 독자들은 아마도 이 교재를 모두 읽은 후에 C++에 대한 자신감으로 혹
은 필요에 의해서 Win95용 응용 프로그램을 개발하려고 할 것이다. 적당한
컴파일러를 선택해야 하는데 Visual C++와 MFC는 최악의 선택 - 필자의
견해에 - 이다. 여러분은 Win95의 내부 시스템을 이해하기 전에, MFC를 이
해하기 위해 대부분의 시간을 소비해야 할 것이다. 왜 표준도 아닌 클래스
라이브러리를 이해하는데 시간과 노력을 투자해야 하는가? 그럼에도 불구
하고 어려운 것을 좋아하는 많은 프로그래머들이 MFC를 배우기 위해 시간을
투자하고 있다. Visual C++를 이용해 Win32용 응용 프로그램을 개발한다면
문제가 아니지만 MFC를 이용할 작정이라면 다시 한번 신중한 선택을 하기
바란다.

  필자의 의견에 볼런드의 C++ 빌더가 최상의 선택이 될 것이다. C++빌더
는 MFC를 몰라도 되며, 윈도우의 자세한 동작에 대해 몰라도 비주얼하게 프
로그램을 개발하는 것이 가능하다. 또한 최근 - 1998년 5월 - 에 발표된 C++
빌더 3.0은 MFC를 거의 완벽하게 지원⼆하며, VCL(visual component library)

            MFC 프로그래머를 끌어들이기 위해 MFC를 완벽하게 지원하고 있다. 하지만 볼런
          드 C++ 빌더를 이용한 최상의 선택은 VCL을 사용하는 것이다.

을 이용한 프로그램 개발은 빠르며 쉽다. 또한 Win32용 응용 프로그램을 개
발할 수 있다. 하지만 C++언어를 완벽하게 지원하는 C++빌더와 윈도우용 라
이브러리로 VCL을 선택하는 것은 최상의 선택⼆이다.

  세부적인 사항까지 건드리는 저 수준의 프로그램을 개발하기 위해서는
Win32가 최상의 선택이다. 이것은 볼런드 C++ 5.0, 비주얼 C++ 5.0 혹은 C++
빌더 모두 지원하므로, 문제가 되는 것은 아니다. 하지만, 컴파일 속도는 볼런
드가 약간 빠르며, 실행 파일의 크기 또한 작다. Win32를 이용하지 않으면서
세부적인 사항까지 건드리는 프로그램을 개발하기 위해서는 MFC가 최선의
선택이라고 한다. 하지만 그렇지 않다. C++ 빌더에서는 Win32의 API를 완벽
하게 지원한다.

            필자는 볼런드의 신봉자가 아니며 마이크로소프트를 미워하는 것도 아니다.

  그럼에도 불구하고 여기서 MFC의 메시지 맵을 예로 드는 것은, 메시지 맵
의 응용이 가상 함수를 적절히 잘 이용한 때문이다.

#include <stdio.h>

#include <iostream.h>

class CObject {

public:

    virtual void OnDraw() {

        cout << "OnDraw() of CObject" << endl;

    }

};//class CObject

class CWnd : public CObject {

public:

    void OnDraw() {

        CObject::OnDraw();

        cout << "OnDraw() of CWnd" << endl;

    }

};//class CWnd

class CView : public CWnd {

public:

    void OnDraw() {

        CWnd::OnDraw();

        cout << "OnDraw() of CView" << endl;

    }

};//class CView

CObject *obj;

class CMyView : public CView {

public:

    void OnDraw();

};//class CMyView

void CMyView::OnDraw() {

    CView::OnDraw();

    cout << "User added OnDraw()" << endl;

}

CMyView view;

void main() {

    obj=&view;

    obj->OnDraw();

}//main

  Hello Windows 95!

Charlez Petzold의 저서 "Programming Windows 95"의 2장에서 소개되는
Hello Windows 95!라는 C프로그램을 메시지 맵을 사용하는 C++ 프로그램으
로 바꾸는 과정을 살펴보자. 함수 포인터와 가상 함수는 얼마나 강력한가!

  Win32의 API(application programming interface)나 MFC에 경험이 없는
독자들은 이 부분을 건너뛰어라.

  아래의 소스는 C소스이다. 이 프로그램을 C++ Builder 혹은 Visual C++에
서 컴파일해서 실행하면, 메뉴를 가지지 않는 윈도우가 하나 만들어지고, 윈도
우 클라이언트 영역(client area)의 중앙에 Hello Windows 95!가 출력된다.

#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,

                         LPARAM lParam) {

    HDC         hdc;

    PAINTSTRUCT ps;

    RECT        rect;

    switch (iMsg) {

    case WM_CREATE:

        return 0;

    case WM_PAINT:

        hdc=BeginPaint(hwnd,&ps);

        GetClientRect(hwnd,&rect);

        DrawText(hdc,"Hello, Windows 95!",-1,&rect,

            DT_SINGLELINE|DT_CENTER|DT_VCENTER);

        EndPaint(hwnd,&ps);

        return 0;

    case WM_DESTROY:

        PostQuitMessage(0);

        return 0;

    }//switch

    return DefWindowProc(hwnd,iMsg,wParam,lParam);

}//WndProc

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,

                   PSTR szCmdLine,int iCmdShow) {

    static char szAppName[]="HelloWin";

    HWND        hwnd;

    MSG         msg;

    WNDCLASSEX  wndclass;

    wndclass.cbSize         =sizeof(wndclass);

    wndclass.style          =CS_HREDRAW|CS_VREDRAW;

    wndclass.lpfnWndProc    =WndProc;

    wndclass.cbClsExtra     =0;

    wndclass.cbWndExtra     =0;

    wndclass.hInstance      =hInstance;

    wndclass.hIcon          =LoadIcon(NULL,IDI_APPLICATION);

    wndclass.hCursor        =LoadCursor(NULL,IDC_ARROW);

    wndclass.hbrBackground  =(HBRUSH)GetStockObject(WHITE_BRUSH);

    wndclass.lpszMenuName   =NULL;

    wndclass.lpszClassName  =szAppName;

    wndclass.hIconSm        =LoadIcon(NULL,IDI_APPLICATION);

    RegisterClassEx(&wndclass);

    hwnd=CreateWindow(szAppName,    //window class name

        "The Hello Program",        //window caption

        WS_OVERLAPPEDWINDOW,        //window style

        CW_USEDEFAULT,              //initial x position

        CW_USEDEFAULT,              //initial y position

        CW_USEDEFAULT,              //initial x size

        CW_USEDEFAULT,              //initial y size

        NULL,                       //parent window handle

        NULL,                       //window menu handle

        hInstance,                  //program instance handle

        NULL);                      //creation parameters

    ShowWindow(hwnd,iCmdShow);

    UpdateWindow(hwnd);

    while (GetMessage(&msg,NULL,0,0)) {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }//while

    return msg.wParam;

}//WinMain

1단계: 무조건 클래스로 바꾼다.

#include <windows.h>

//Forward declaration------------------------------------------------

LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,

                         LPARAM lParam);

//Class definition---------------------------------------------------

class CApp {

    static char szAppName[];

    HWND        hwnd;

    MSG         msg;

    WNDCLASSEX  wndclass;

public:

    void InitInstance(HINSTANCE hInstance,PSTR szCmdLine,

                         int iCmdShow);

    void Run();

    WPARAM ExitInstance();

    void OnCreate();

    void OnDraw();

    void OnDestroy();

};//class CApp

void CApp::InitInstance(HINSTANCE hInstance,PSTR szCmdLine,

                        int iCmdShow) {

    wndclass.cbSize         =sizeof(wndclass);

    wndclass.style          =CS_HREDRAW|CS_VREDRAW;

    wndclass.lpfnWndProc    =WndProc;

    wndclass.cbClsExtra     =0;

    wndclass.cbWndExtra     =0;

    wndclass.hInstance      =hInstance;

    wndclass.hIcon          =LoadIcon(NULL,IDI_APPLICATION);

    wndclass.hCursor        =LoadCursor(NULL,IDC_ARROW);

    wndclass.hbrBackground  =(HBRUSH)GetStockObject(WHITE_BRUSH);

    wndclass.lpszMenuName   =NULL;

    wndclass.lpszClassName  =szAppName;

    wndclass.hIconSm        =LoadIcon(NULL,IDI_APPLICATION);

    RegisterClassEx(&wndclass);

    hwnd=CreateWindow(szAppName,    //window class name

        "The Hello Program",        //window caption

        WS_OVERLAPPEDWINDOW,        //window style

        CW_USEDEFAULT,              //initial x position

        CW_USEDEFAULT,              //initial y position

        CW_USEDEFAULT,              //initial x size

        CW_USEDEFAULT,              //initial y size

        NULL,                       //parent window handle

        NULL,                       //window menu handle

        hInstance,                  //program instance handle

        NULL);                      //creation parameters

    ShowWindow(hwnd,iCmdShow);

    UpdateWindow(hwnd);

}//CApp::InitInstance

void CApp::Run() {

    while (GetMessage(&msg,NULL,0,0)) {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }//while

}//CApp::Run

WPARAM CApp::ExitInstance() {

    return msg.wParam;

}//CApp::ExitInstance

char CApp::szAppName[]="HelloWin";

//Event handler------------------------------------------------------

void CApp::OnCreate() {

}//CApp::OnCreate

void CApp::OnDraw() {

    HDC         hdc;

    PAINTSTRUCT ps;

    RECT        rect;

    hdc=BeginPaint(hwnd,&ps);

    GetClientRect(hwnd,&rect);

    DrawText(hdc,"Hello, Windows 95!",-1,&rect,

        DT_SINGLELINE|DT_CENTER|DT_VCENTER);

    EndPaint(hwnd,&ps);

}//CApp::OnDraw

void CApp::OnDestroy() {

    PostQuitMessage(0);

}//CApp::OnDestroy

//Global object------------------------------------------------------

CApp app;

//Window procedure---------------------------------------------------

LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,

                         LPARAM lParam) {

    switch (iMsg) {

    case WM_CREATE:

        app.OnCreate();

        return 0;

    case WM_PAINT:

        app.OnDraw();

        return 0;

    case WM_DESTROY:

        app.OnDestroy();

        return 0;

    }//switch

    return DefWindowProc(hwnd,iMsg,wParam,lParam);

}//WndProc

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,

                   PSTR szCmdLine,int iCmdShow) {

    app.InitInstance(hInstance,szCmdLine,iCmdShow);

    app.Run();

    return app.ExitInstance();

}//WinMain

2단계: 메시지를 다루는 부분만을 하위 클래스에서
조작할 수 있도록 한다.

가상 함수의 성질을 이용하면, 윈도우 프로시져를 자동화할 수 있다.

#include <windows.h>

//Forward declaration------------------------------------------------

LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,

                         LPARAM lParam);

//Class CObject------------------------------------------------------

class CObject {

protected:

    static char szAppName[];

    HWND        hwnd;

    MSG         msg;

    WNDCLASSEX  wndclass;

public:

    void InitInstance(HINSTANCE hInstance,PSTR szCmdLine,

                         int iCmdShow);

    void Run();

    WPARAM ExitInstance();

    virtual void OnCreate()=0;

    virtual void OnDraw()=0;

    virtual void OnDestroy()=0;

    virtual void OnLButtonDown()=0;

};//class CObject

void CObject::InitInstance(HINSTANCE hInstance,PSTR szCmdLine,

                        int iCmdShow) {

    wndclass.cbSize         =sizeof(wndclass);

    wndclass.style          =CS_HREDRAW|CS_VREDRAW;

    wndclass.lpfnWndProc    =WndProc;

    wndclass.cbClsExtra     =0;

    wndclass.cbWndExtra     =0;

    wndclass.hInstance      =hInstance;

    wndclass.hIcon          =LoadIcon(NULL,IDI_APPLICATION);

    wndclass.hCursor        =LoadCursor(NULL,IDC_ARROW);

    wndclass.hbrBackground  =(HBRUSH)GetStockObject(WHITE_BRUSH);

    wndclass.lpszMenuName   =NULL;

    wndclass.lpszClassName  =szAppName;

    wndclass.hIconSm        =LoadIcon(NULL,IDI_APPLICATION);

    RegisterClassEx(&wndclass);

    hwnd=CreateWindow(szAppName,    //window class name

        "The Hello Program",        //window caption

        WS_OVERLAPPEDWINDOW,        //window style

        CW_USEDEFAULT,              //initial x position

        CW_USEDEFAULT,              //initial y position

        CW_USEDEFAULT,              //initial x size

        CW_USEDEFAULT,              //initial y size

        NULL,                       //parent window handle

        NULL,                       //window menu handle

        hInstance,                  //program instance handle

        NULL);                      //creation parameters

    ShowWindow(hwnd,iCmdShow);

    UpdateWindow(hwnd);

}//CObject::InitInstance

void CObject::Run() {

    while (GetMessage(&msg,NULL,0,0)) {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }//while

}//CObject::Run

WPARAM CObject::ExitInstance() {

    return msg.wParam;

}//CObject::ExitInstance

char CObject::szAppName[]="HelloWin";

CObject* pCObject;

//class CView--------------------------------------------------------

class CView : public CObject {

public:

    void OnCreate();

    void OnDraw();

    void OnDestroy();

    void OnLButtonDown();

};//class CView

//CView Event handler------------------------------------------------

void CView::OnCreate() {

}//CView::OnCreate

void CView::OnDraw() {

    HDC         hdc;

    PAINTSTRUCT ps;

    RECT        rect;

    hdc=BeginPaint(hwnd,&ps);

    GetClientRect(hwnd,&rect);

    DrawText(hdc,"Hello, Windows 95!",-1,&rect,

        DT_SINGLELINE|DT_CENTER|DT_VCENTER);

    EndPaint(hwnd,&ps);

}//CView::OnDraw

void CView::OnDestroy() {

    PostQuitMessage(0);

}//CView::OnDestroy

void CView::OnLButtonDown() {

    MessageBox(NULL,"MESSAGE","TITLE",MB_ICONEXCLAMATION|MB_OK);

}//CView::OnLButtonDown

//Global object------------------------------------------------------

CView app;

//Window procedure---------------------------------------------------

LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,

                         LPARAM lParam) {

    switch (iMsg) {

    case WM_CREATE:

        pCObject->OnCreate();

        return 0;

    case WM_PAINT:

        pCObject->OnDraw();

        return 0;

    case WM_DESTROY:

        pCObject->OnDestroy();

        return 0;

    case WM_LBUTTONDOWN:

        pCObject->OnLButtonDown();

        return 0;

    }//switch

    return DefWindowProc(hwnd,iMsg,wParam,lParam);

}//WndProc

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,

                   PSTR szCmdLine,int iCmdShow) {

    pCObject=&app;

    pCObject->InitInstance(hInstance,szCmdLine,iCmdShow);

    pCObject->Run();

    return pCObject->ExitInstance();

}//WinMain

3단계: 함수 포인터 테이블을 이용하여 완전히 자동
화한다.

#include <windows.h>

//Forward declaration------------------------------------------------

LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,

                         LPARAM lParam);

//class CObject;

//typedef void (CObject::*CObjectFunPointer)();

//static CObjectFunPointer fpCObjectGlobal;

//CObject* pCObject;

//Class CObject------------------------------------------------------

class CObject {

protected:

    static char szAppName[];

    HWND        hwnd;

    MSG         msg;

    WNDCLASSEX  wndclass;

public:

    void InitInstance(HINSTANCE hInstance,PSTR szCmdLine,

                         int iCmdShow);

    void Run();

    WPARAM ExitInstance();

};//class CObject

void CObject::InitInstance(HINSTANCE hInstance,PSTR szCmdLine,

                        int iCmdShow) {

    wndclass.cbSize         =sizeof(wndclass);

    wndclass.style          =CS_HREDRAW|CS_VREDRAW;

    wndclass.lpfnWndProc    =WndProc;

    wndclass.cbClsExtra     =0;

    wndclass.cbWndExtra     =0;

    wndclass.hInstance      =hInstance;

    wndclass.hIcon          =LoadIcon(NULL,IDI_APPLICATION);

    wndclass.hCursor        =LoadCursor(NULL,IDC_ARROW);

    wndclass.hbrBackground  =(HBRUSH)GetStockObject(WHITE_BRUSH);

    wndclass.lpszMenuName   =NULL;

    wndclass.lpszClassName  =szAppName;

    wndclass.hIconSm        =LoadIcon(NULL,IDI_APPLICATION);

    RegisterClassEx(&wndclass);

    hwnd=CreateWindow(szAppName,    //window class name

        "The Hello Program",        //window caption

        WS_OVERLAPPEDWINDOW,        //window style

        CW_USEDEFAULT,              //initial x position

        CW_USEDEFAULT,              //initial y position

        CW_USEDEFAULT,              //initial x size

        CW_USEDEFAULT,              //initial y size

        NULL,                       //parent window handle

        NULL,                       //window menu handle

        hInstance,                  //program instance handle

        NULL);                      //creation parameters

    ShowWindow(hwnd,iCmdShow);

    UpdateWindow(hwnd);

}//CObject::InitInstance

void CObject::Run() {

    while (GetMessage(&msg,NULL,0,0)) {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }//while

}//CObject::Run

WPARAM CObject::ExitInstance() {

    return msg.wParam;

}//CObject::ExitInstance

char CObject::szAppName[]="HelloWin";

//class CView--------------------------------------------------------

class CView;

typedef void (CView::*CViewFunPointer)();

typedef struct tagMessageMap {

    UINT iMsg;

    CViewFunPointer fp;

} MessageMap;

static CViewFunPointer fpCViewGlobal;

//CView* pCView;

class CView : public CObject {

public:

    static MessageMap messageMap[];

public:

    void OnCreate();

    void OnDraw();

    void OnDestroy();

};//class CView

//{{BEGIN_MESSAGE_MAP

MessageMap CView::messageMap[]={

    {WM_CREATE,CView::OnCreate},

    {WM_PAINT,CView::OnDraw},

    {WM_DESTROY,CView::OnDestroy},

    {0,NULL}

};

//}}END_MESSAGE_MAP

//CView Event handler------------------------------------------------

void CView::OnCreate() {

}//CView::OnCreate

void CView::OnDraw() {

    HDC         hdc;

    PAINTSTRUCT ps;

    RECT        rect;

    hdc=BeginPaint(hwnd,&ps);

    GetClientRect(hwnd,&rect);

    DrawText(hdc,"Hello, Windows 95!",-1,&rect,

        DT_SINGLELINE|DT_CENTER|DT_VCENTER);

    EndPaint(hwnd,&ps);

}//CView::OnDraw

void CView::OnDestroy() {

    PostQuitMessage(0);

}//CView::OnDestroy

//Global object------------------------------------------------------

CView app;

//Window procedure---------------------------------------------------

LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,

                         LPARAM lParam) {

    int i=0;

    while (CView::messageMap[i].iMsg!=0) {

        if (iMsg==CView::messageMap[i].iMsg) {

            fpCViewGlobal=CView::messageMap[i].fp;

            (app.*fpCViewGlobal)();

            return 0;

        }//if

        ++i;

    }//while

    return DefWindowProc(hwnd,iMsg,wParam,lParam);

}//WndProc

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,

                   PSTR szCmdLine,int iCmdShow) {

    app.InitInstance(hInstance,szCmdLine,iCmdShow);

    app.Run();

    return app.ExitInstance();

}//WinMain

  Pointer to Member operator: .* and ->*

#include <stdio.h>

#include <iostream.h>

//#pragma pointers_to_members(full_generality)

class CObject {

    int i;

public:

    CObject(int t=0):i(t) {}

    void Add(int k) { i+=k; }

    void Show() { cout << i << endl; }

};//class CObject

void (CObject::*pMemberFunc)(int)=CObject::Add;

void main() {

    CObject obj;

    //CObject* pObject=&obj;

    //pObject->pMemberFunc=&CObject::Add;

    //(pObject->*pMemberFunc)(3);

    //obj.pMemberFunc=CObject::Add;

    (obj.*pMemberFunc)(3);

    obj.Show();

}//main

#include <stdio.h>

#include <iostream.h>

class CObject {

    int i;

public:

    CObject(int t=0):i(t) {}

    void Add(int k) { i+=k; }

    void Show() { cout << i << endl; }

};//class CObject

void (CObject::*pMemberFunc)(int)=CObject::Add;

void main() {

    CObject obj1,obj2;

    CObject* pObject=&obj2;

    (pObject->*pMemberFunc)(3);

    pObject->Show();

}//main

  Error: Undeclared Identifier

#include <stdio.h>

#include <iostream.h>

class CObject {

    int i;

public:

    void (CObject::*pMemberFunc)(int);

public:

    CObject(int t=0):i(t) {}

    void Add(int k) { i+=k; }

    void Show() { cout << i << endl; }

};//class CObject

void main() {

    CObject obj1,obj2;

    CObject* pObject=&obj2;

    pObject->pMemberFunc=CObject::Add;

    (pObject->*pMemberFunc)(3);//error!

    pObject->Show();

}//main

  가상 함수 테이블(VTable: virtual function
table): 진보된 주제

  안타깝게도, vtable이 메모리 상에 어떻게 만들어지며, 객체가 가상 함수 호
출을 바인딩하기 위해 어떤 메커니즘을 사용하는지 상세히 설명한 문서를 접
한 적이 없다. 여기서 언급하는 내용은 C++ Builder 3.0의 도움말과 인터넷
(Internet)에서 찾은 몇 가지 자료를 근거로 필자의 생각을 정리한 것임을 밝
혀 둔다.

  모르는 사항이 있을 때, 인터넷은 좋은 스승이다. 아래의 예를 살펴보자.

#include <iostream.h>

#include <typeinfo.h>

class A {

  int i;

public:

  virtual void wow(){ cout << "wow" << endl; }

  virtual void poo(){ cout << "poo" << endl; }

};//class A

class B : public A {

  int i;

public:

  virtual void wow(){ cout << "wow wow" << endl; }

  virtual void haha(){ cout << "haha" << endl; }

};//class B

void main() {

    A* a;

    B b;

    a=&b;

    cout << typeid(*a).name() << endl;

      //RTTI의 typeid의 역할을 설명하기 위해 적어 두었다. 이 내용은 '새

      //로운 형태의 형변환'과 'RTTI'에서 설명한다. 이 줄은 무시해도 좋

      //다.

    a->wow();

    a->poo();

      //This statement translated as (*typeid(*a).vtable[1])();

}

  실행 결과는

    B

    wow wow

    poo

이다.

  1개 이상의 가상함수를 포함한 클래스를 위해, 컴파일러는 가상 함수 테이
블을 할당하고 초기화하는 코드를 생성할 것이다. 물론 가상 함수 테이블
(VTable)은 클래스마다 1개씩 유지될 것이다.

  위의 예에서 클래스 A와 B를 위한 VTable의 모양은 아래와 같다■.

             사실인 것처럼 문체를 계속 사용하겠지만, 이것은 사실이 아닐 수도 있
          다.

                 클래스 A와 B의 VTable: 이 정보는 실행시에 구축되어 동적으로 테이블
          이 만들어진다.

  B의 VTable[]이 A::poo의 주소를 가지고 있음을 주목하라. 이러한 정보는
실행시에 구축된다. 이러한 실행시 클래스 구조나 형(type)에 관한 정보는 컴
파일러가 생성한 RTTI(run-time type information) 엔진에 의해 유지된다.

  위의 프로그램이 실행되었을 때 메모리의 모양은 아래와 같을 것이다.

                 프로그램의 메모리 상태: 클래스 A,B의 모든 멤버 함수의 코드가 생성되
          었으며, A,B 모두 가상 함수를 가지므로, 가상 함수 테이블이 각각 1500번
          지와 1600번지에 만들어졌다.

  A의 멤버 함수의 코드가 각각 1000번지, 1100번지에 할당되었다고 하자. 그
리고 A의 VTable[]이 1500번지에 할당되었다고 하자. VTable[]의 각 엔트리
는 [1000]과 [1100]을 가리킨다. 이것은 VTable[0]이 A::wow()의 시작 주소를,
VTable[1]이 A::poo()의 시작 주소를 가리킴을 의미한다.

  B에 대해서도 유사한 VTable[]이 위 그림처럼 만들어진다. 하지만, B의
VTable[]에는 상위 클래스 A::poo()의 주소도 유지되고 있다. 물론 이러한 정
보는 RTTI 엔진이 동적으로 구축할 것이다.

  이제 객체가 어떻게 할당되는지 살펴보자. 아래 문장은 B의 객체 b를 메모
리에 할당한다. 그리고 메모리의 모양은 아래 그림과 같을 것이다.

    B b;

                 객체 b의 메모리 할당: 틀렸다!

  실제 위 그림은 필자가 이제껏 메모리에 할당된 객체를 나타내기 위해 그린
그림이지만, 이것은 틀린 그림이다. 실제 - 가상 함수를 포함하는 - 객체의
모양은 아래 그림과 같다.

                 객체 b의 메모리 할당: 가상 함수 포인터를 위해 여분의 4바이트가 더 할
          당된다. 하지만, 이것은 sizeof()에 영향을 미치지 않는다.

  가상 함수를 포함하는 클래스의 객체는 가상 함수 테이블의 시작 주소를 가
리키는 포인터가 더 할당된다. 하지만, 이것이 sizeof()에는 영향을 미치지 않
으므로 - 멤버 함수의 호출에 this가 명시적으로 보이지 않는 것처럼 -
sizeof(b)는 32비트 운영체제에서 여전히 4(8이 아니라)이다. 위의 예에서
B::VTable[]의 주소는 [1600]이다.

  객체의 멤버에 보이지 않는 VTable이 포함된다는 것은 멤버 함수의 호출이
컴파일 시간이 아니라, 실행 시간에 결정됨을 의미한다. 다른 멤버 함수의 호
출은 컴파일 시간에 함수의 주소가 결정되지만, 가상 함수는 실행시에 만들어
진 VTable에 의해 함수 호출의 주소가 결정된다. 이것은 가상 함수가 일반
멤버 함수보다 약간 느린 이유이다. 아래의 그림을 보자.

                 객체 b가 할당된 메모리 그림: b의 VTable 멤버는 클래스 B의 가상 함수
          테이블의 주소를 가리킨다.

  이제 main()의 문장들이 어떻게 수행되는지 살펴보자.

    A* a;

    B b;

  위 문장에 의해 a가 5000번지에, b는 5004∼5011번지에 할당된다.

    a=&b;

  a가 객체 b의 시작 주소를 가리키게 된다. a의 정적 타입 - 컴파일 시간에
결정된 - 은 A*이다. 하지만, 실행시에는 B*로 해석되어야 한다.

    cout << typeid(*a).name() << endl;

  새로운 키워드 typeid는 실행시 객체의 형을 구분할 수 있다.

    a->wow();

    a->poo();

  위 문장은 각각

    a가 가리키는 형의 멤버 함수 wow()호출;

    a가 가리키는 형의 멤버 함수 poo()호출;

로 컴파일되어야 한다. 이것은 실제 다음과 같이 번역될 것이다.

    (*typeid(*a).vtable[0])();

    (*typeid(*a).vtable[1])();

  위의 문장이 실제로 컴파일되는 것은 아니지만, 가상 함수의 호출 메커니즘
을 설명하기에는 충분하다. 요지는 다음과 같다.

  클래스의 모든 가상 함수는 적당한 방법으로 - 예를 들면 함수의 이름
- 정렬(sort)되어 실행시 가상함수 테이블에 유지된다. 그리고 가상
함수의 호출은 가상 함수 테이블의 인덱스 참조로 해결되는 것이다.

  그럼에도 불구하고, wow()의 주소가 VTable[0]에 poo()의 주소가
VTable[1]에 있다는 것을 어떻게 아는지에 관한 것은 여전히 의문이다■. 만약

                 가상 함수 테이블은 실행 시에 만들어지므로, 컴파일 시간에 wow()의 인
          덱스가 0인지 결정할 수 없다. 만약 함수의 이름으로 정렬되어 가상 함수
          테이블이 유지되고, 테이블에 있는 가상 함수 엔트리가 모두 n개라면, 가
          상 함수 호출은 함수의 주소를 찾기 위해 여분의 logn시간(이진 탐색)을 사
          용해야 한다. 이것은 큰 오버헤드이다. 실제 컴파일러 벤더는 이 같은 방
          법으로 가상 함수 호출을 해결하지는 않을 것이다.

wow() 혹은 poo()의 주소를 찾기 위해 가상 함수 테이블을 찾아야 한다면,
함수 호출은 더 느려진다.

  외워야 할 표(table)

  virtual 이라는 키워드를 필요로 하는 경우를 정리해 보면 아래 표와 같다.
아래의 표를 외우는 핵심은 virtual이 항상 늦은 바인딩(late binding), 즉 실행
시간 바인딩(run-time binding)과 상관되어 있다는 것이다.

  또한 스택 상에 객체가 만들어지는 경우만으로는 virtual이 아무런 의미가
없다. 상위 클래스를 가리키는 포인터가 이미 만들어진 하위 클래스를 가리키
는 경우, 혹은 상위 클래스를 가리키는 포인터가 동적으로 힙(heap)에 만들어
진 객체를 가리키는 경우, virtual이 동작한다.

핵심

예

virtual function

subtype principle이 사용
된 경우, 하위 클래스의
함수가 실행되도록 해 준
다. 또한 순수 가상 함수
를 기억하라. 순수 가상
함수는 추상 클래스를 만
든다.

class A {

...

virtual void Fun();

...

};

//in case of pure virtual

//function

...

virtual void Fun()=0;

virtual base class

다중 상속(multiple
inheritance)에서 공통 조
상을 가지는 경우, 공통
조상의 생성자가 한 번만
실행되도록 해 준다.

class A {

...

};

class B : virtual public
A {

...

};

class C : virtual public
A {

...

};

virtual destructor

subtype principle이 사용
된 경우, 하위 클래스의
소멸자부터 실행되도록
해 준다. 소멸자에서 동
적으로 할당한 메모리를
해제하는 경우, 반드시
가상 소멸자로 선언해야
한다.

class A {

...

    virtual ~A();

...

};

  연습문제

1. 이 장에서 가상 베이스 클래스를 사용하지 않았을 때의 메모리 샘에 관한
설명은 틀리다(메모리 샘은 발생하지 않는다). 아래 그림을 참고하여 올바른
설명을 하라.

2.


44. this 포인터

44. this 포인터와
mutable

  C++에 새로 도입된 키워드 this는 어떤 역할을 하는가? this를 이해하는 것
이, 멤버 변수와 멤버 함수를 포함한 객체가 어떻게 메모리에 할당되고, 어떻
게 적절한 멤버 함수를 호출하며, 멤버 변수를 참조하는지를 이해하는 핵심이
다. this를 정의하면,

"할당된 객체의 시작 포인터"

이다. 즉, 정수형 변수가 할당되었을 때의 시작 주소는 정수형 포인터로 참조
한다. 함수의 시작 주소는 함수 포인터로 참조해야 한다. 객체의 시작 주소는
객체 포인터(object pointer)로 참조한다. 하지만, this는 객체 포인터이기는
하지만, 성격이 좀 다르다. this는 컴파일러에 의해서, 실행 시간(run-time)에
자동으로 계산되어지는 객체의 시작 주소이다. 즉 어떤 객체가 만들어지면, 각
각의 객체마다 this포인터를 위한 메모리 공간이 할당된다. 또한 멤버 함수를
호출할 때마다, this 포인터는 묵시적(implicit)으로 멤버 함수에 자동으로 전
달된다1).

  1) 여러분이 파라미터를 전달받지 않는 멤버 함수를 작성해도 최소한 this 한개는 전달

    된다는 의미이다. 이것은 함수의 오버헤드(overhead)이다. 가상 함수(virtual function)를

    멤버 함수로 가지는 경우 이러한 오버헤드는 더 커진다.

  문제는 여기에 있다.

"객체를 만들 때, 멤버 함수도 매 객체마다 만들어야 하는가?"

  독자들은 이 문제의 답이 NO!라는 것을 알고 있을 것이다. 그렇다면, 객체
가 만들어 질 때 실제로 만들어지는 것은 멤버 함수가 아니고 무엇인가? 또
한 그렇다면, 멤버 함수가 호출 때마다 어떻게 적절한 객체의 멤버 변수들을
참조하는가? 이러한 두 가지 문제를 해결한 것이 바로 this 포인터이다.

  간단한 예를 통하여, 실제로 객체가 메모리에 할당되는 것을 살펴보자. 아래
의 CTest 클래스는 간단한 2개의 정수 멤버 변수를 가지며, 생성자는 이 값
들을 초기화 한다. 또한 Print()는 이 정수 값들을 출력한다.

#include <stdio.h>

class CTest {

    int i,j;

  public:

    CTest(int ti=0,int tj=0) {

        i=ti;

        j=tj;

    }

    void Print() {

        printf("i=%d,j=%d\n",i,j);

    }

};

void main() {

    CTest a(1,2), b(3,4);

    b.Print();

}

  결과는 물론, i=3,j=4이다. 그렇다면, CTest의 객체 a와 b는 메모리에 과연
어떻게 할당되는가?

  컴파일러는 클래스의 멤버 함수에 대해 특별한 방식으로 코드를 생성한다.
생성자 CTest()나 멤버 함수 Print()가 사용하는 i와 j가 메모리의 어디에 할
당되었는지 어떻게 알 수 있는가? 모른다. 하지만, 실제로 객체가 할당되었다
면, i는 할당된 객체의 시작 주소 - 이것을 this라 하자 - 에서 상대주소로
0∼1에 걸쳐 있을 것이다2). 그리고 j는 2∼3에 걸쳐 있을 것이다3). 그러므로

  2) int는 메모리에 2바이트 할당된다. 하지만 이것이 항상 참은 아니다. Win95용의 32비

    트 컴파일러에서는 int를 위해 4바이트의 메모리를 사용한다. 하지만, 우리들은 int를 2

    바이트라 가정할 것이다.

  3) 실제로 첫번째 변수 - 여기서는 i - 는 두번째 오프셋(offset)에 위치한다. 첫번째 오

    프셋에는 가상 함수를 위해 가상 함수 테이블(virtual table; v-table)을 가리키는 포인

    터가 항상 유지된다.

i와 j에 대해 각각 다음과 같은 코드를 생성한다.

[this+0]4)

  4) 이것이 정수가 아니고 주소(address)임을 확실히 하기 위해 대괄호([,])를 사용하기로

    하자. 또한 주소 a가 가리키는 내용을 위해 [a]*와 같은 표기법을 사용할 것이다.

[this+2]

  그러면 생성자 CTest()의 코드는 다음과 같이 생성될 것이다.

[this+0]=[sp+0]*5)

[this+2]=[sp+1]*

  5) sp는 스택 포인터(stack pointer)이다. 함수 호출시에 파라미터들은 스택을 통해 전달

    된다. 또한 C 계열의 함수들은 파라미터를 스택에 푸시(push)할 때, 오른쪽에서 왼쪽으

    로(right to left) 푸시한다. 일반적으로 스택은 2byte단위로 동작한다. 그러므로 스택의

    상단에는 ti가 있고, ti의 바로 밑에는 tj가 있다. sp는 int*의 역할을 하므로, sp+0은 ti를

    가리킨다. sp+1은 tj를 가리킨다. 여러분은 int*가 2단위로 증가하는 것을 알고 있을 것

    이다. 이것은 다음과 같은 문장이 제대로 동작하는 이유이다.

  printf("%d,%d\n", 'a', 'b');

        이 문장은 훌륭하게 97,98을 출력한다. %d가 2바이트의 정수를 찍는다는 사실에 주

      목하라. 그리고 'a'는 1바이트 표현임에 주목하라. 'a'가 스택에 들어갈 때, 1바이트로

      들어갔다면, 첫 번째 %d에 의해, 연속된 'a','b'를 정수화한 값이 출력될 것임에 분명하

      다. 하지만, 'a'는 비록 표현은 1바이트이기는 하지만, 스택에 들어갈 때 2바이트로 들

      어 간다. 조언은 다음과 같다. 스택 동작을 줄이기 위해 굳이 char를 파라미터로 전달

      하려고 시도하지 말라. char를 전달하는 것은 int를 전달하는 오버헤드와 같다.

  Print()또한 마찬가지이다. 이렇게 멤버 함수가 참조하는 멤버 변수마다에
대해, this에 의한 상대주소로 코드가 생성된다. 그렇다면 객체가 메모리에 할
당될 때, 무엇이 할당되는가? 멤버 변수가 먼저 할당된다. 그리고 멤버 함수에
대해, 함수의 모든 코드가 할당되는 것이 아니라, 함수의 시작 주소만이6)

  6) 아마도 시작 주소이외에 호출을 위한 여분의 정보도 할당될 것이다.

할당된다. 생성자 CTest()의 시작 주소가 1000, Print()의 시작 주소가 1500이
였다고 하자. 그러면, 객체 a가 만들어 질때, i,j를 위해 4바이트, 2개의 함수의
시작 주소를 위해 8바이트7)를 사용하여, 모두 12바이트가 할당된다. a와 b

  7) 포인터는 4바이트이다. 물론 이것은 운영체제의 의존적이지만, Win95는 32비트 선형

    주소를 사용한다. 여러분이 DOS에서 small모델을 사용한다면 2바이트가 될지도 모른

    다고 생각할지 모른다. 하지만, 함수는 일반적으로 다른 세크먼트에 위치하므로 4바이

    트일 것이다.

는 블락(block)안에서 선언된 지역 객체(local object)이므로, 스택에 할당된다.
a가 할당된 곳이 5000이였다면, b는 4988번지에 할당된다. 아래 그림이 두 객
체가 할당된 상태의 메모리 맵(map)을 보여준다.

  그림 44-1 a와 b가 할당된 상태의 메모리; a가 할당되자 마자 a의 생성자가 호출되므

    로, a의 멤버 변수는 초기화되어 있다. 하지만, b의 생성자는 아직 호출되지 않은 상태

    이다.

  객체 a의 생성자가 호출될 때, this는 5000으로 전달된다. b의 생성자가 호
출될 때, this는 4988으로 전달된다. 이것이 멤버 함수가 적절한 멤버 변수를
참조하는 방법이다!

  그러므로 명시적으로 this를 써 줄 수 있음에 주목하라. this는 포인터이므
로 화살표 연산자(->)를 사용하며 멤버 함수나 변수를 참조하는 것이 가능하
다. 그러므로 생성자 CTest()는 다음과 같이 수정할 수 있다.

    CTest(int ti=0,int tj=0) {

        this->i=ti;

        this->j=tj;

    }

  전혀 필요가 없는 것 같은 this가 반드시 사용되어야 하는 곳이 있다. 멤버
함수가 객체 자신을 리턴하는 것을 어떻게 구현할 것인가? 멤버 함수 Get()을
추가하기로 하자. Get()은 객체를 리턴한다. 여러분은 CTest 클래스 정의안에
Get()을 다음과 같이 추가할 수 있을 것이다.

    CTest Get() {

        return *this;

    }

  지금은 이 함수가 아무일도 하지 않는다. 단지 객체를 리턴할 뿐이다. 하지
만, 여러분은 연산자 오버로딩(operator overloading)에서 그리고 참조를 리턴
하는 함수에서 this가 얼마나 중요하게 쓰이는지 곧 알게 될 것이다.

  반드시 this가 필요한 경우

//{{yet specified...

  상수 멤버 함수(const member function)와
mutable

#include <iostream.h>

#include <string.h>

class X {

    mutable int age;

    char name[80];

  public:

    X(const char*, int = 0);

    void Show() const {

        age++;//this is error if age isn't mutable

        cout << age << endl;

        cout << name << endl;

    }

};

X::X(const char* n,int a) {

    strcpy(name,n);

    age=a;

}

void main() {

    X a("seojt",30);

}

  const 멤버 함수에서도 조작 가능한 변덕장
이(mutable) 변수

Syntax

mutable <variable name>;

Description

Use the mutable specifier to make a variable modifiable even though it is
in a const-qualified expression.

Using the mutable Keyword

Only class data members can be declared mutable. The mutable keyword
cannot be used on static or const names. The purpose of mutable is to
specify which data members can be modified by const member functions.
Normally, a const member function cannot modify data members.

  실습문제

1.


메타 클래스(meta class)

45. 메타 클래스(meta
class)

  메타 언어(meta language)는 언어를 기술하는 언어를 말한다. 영어의 문
법을 기술하기 위해서 우리는 무슨 언어를 사용해야 하는가? 영어의 문법을
기술하는 언어를 사용해야 할 것이다. 이처럼 언어 뒤의 언어를 메타 언어라
한다. 일반적으로 메타가 의미하는 것은 '그것의 뒤의'이란 의미이다. 메타 클
래스는 클래스를 기술하는 클래스를 의미하며, 이것은 C++의 새로운 키워드
template로 시작한다. 이러한 메타 클래스는 다른 책에서 일반화된 클래스
(generic class), 파라미터를 받는 클래스(parameterized class), 템플릿
클래스(template class)로 표현된다. C++의 키워드가 template이므로 템플릿
이란 용어를 사용하는 것이 일반적이다. 또한 이 책에서도 이 용어 - 비록 독
자들의 환기를 끌기 위해 제목에는 메타 클래스를 사용하기는 했지만 - 를
사용할 것이다.

  우리가 먼저 살펴볼 템플릿은 함수 템플릿(function template)이다.

  두 개의 변수 값을 바꾸기 위해 Swap()이란 함수를 만들 필요1)가 종종

  1) 흔히 정렬(sorting) 알고리즘에서 이러한 문제가 발생한다. 버블 정렬(bubble sorting)인

    경우 연속한 두 개의 값을 바꾸기 위해 Swap()을 사용할 것이다.

있다. 이것은 파라미터를 두 개 받아 이 두 변수의 값을 교환(swapping)한다.
바꾸어야 할 변수가 정수(integer)라면, 독자들은 아래와 같은 소스를 작성할
것이다.

    void Swap(int &2)i, int &j) {

  2) &는 C++에서 참조에 의한 호출(call by reference)를 나타내는 문법 기호(symbol)이다.

    이것은 파라미터로 넘어간 변수의 값이 함수 내부에서 바뀌면, 함수를 종료하여 빠져

    나왔을 때에도 값이 바뀌어 있다는 것을 보장함을 의미한다.

        int t=i;

        i=j;

        j=t;

    }//Swap3)

  3) 여러분은 임시 변수 t를 사용하지 않고 두 변수의 값을 Swap하는 방법을 알고 있는

    가? 다음의 문장이 바로 그것이다. a=a+b; b=a-b; a=a-b; 덧셈/뺄셈의 오버헤드가 싫다

    면, a=a^b; b=a^b; a=a^b; 를 사용할 수도 있을 것이다. 이것은 덧셈/뺄셈에 비해 훨씬

    빠르다. 이유는 '2진수, 2의 보수, 비트 플래그'를 보라.

  만약 i=2,j=3 일 때, Swap(i,j)를 호출하면, i=3,j=2가 될 것이다. 이것은 훌륭
히 자신의 기능을 수행한다. 하지만, 바꾸어야할 변수의 형(type)이 long이라
면, 독자들은 위의 소스를 아래와 같이 수정하여야 한다.

    void Swap(long &i, long &j) {

        long t=i;

        i=j;

        j=t;

    }//Swap

  이와 같은 수정이 귀찮은 독자들은 Swap()해야할 형(type)을 명시하기 위
해 typedef를 사용하거나 #define 매크로를 사용할지도 모른다.

    //#define SwapType long

    typedef long SwapType;

    void Swap(SwapType &i, SwapType &j) {

        SwapType t=i;

        i=j;

        j=t;

    }//Swap

  위의 소스 코드는 훌륭하다. 만약 Swap()이 char형을 필요로 한다면,

typedef long SwapType;

를

typedef char SwapType;

로 바꾸면 될 것이다.

  하지만, int를 위한 Swap()과 long을 위한 Swap()이 동시에 필요하다면 어
떻게 할 것인가? 함수를 2개 만드는 것밖에 다른 방법은 없다. C++에서는 함
수 오버로딩(overloading)을 지원하므로 같은 Swap()이란 이름을 사용할 수
있으므로 다행이다.

    void Swap(int &i, int &j) {

        int t=i;

        i=j;

        j=t;

    }//Swap

    void Swap(long &i, long &j) {

        long t=i;

        i=j;

        j=t;

    }//Swap

  여기에 논쟁의 핵심이 있다. 왜 같은 일을 하는 함수를 형이 다르다고 해서
두 번씩이나 만들어야 하는가? C++의 template은 이와 같은 비 효율적인 반
복 작업을 하지 않도록 도와준다. template을 이용하면, 변수의 형(type)을 입
력으로 받아 Swap()함수를 마구 찍어낼 수 있다. 우리가 정의해야 하는 것은
Swap()이라는 함수를 찍어내는 틀(template)뿐이다. 이것이 왜 template란 용
어가 키워드인 이유이다. 데이터 형이 SwapType이라면, 소스를 아래와 같이
작성할 것이다.

    void Swap(SwapType &i, SwapType &j) {

        SwapType t=i;

        i=j;

        j=t;

    }//Swap

여기에 우리가 추가해야 하는 코드는 SwapType을 지정해 주는 template 코
드 부분이다. 이것은 함수의 제일 앞에

template<class SwapType>

을 적어주면 된다. 수정된 소스는 아래와 같다.

template<class SwapType> void Swap(SwapType &i, SwapType &j) {

    SwapType t=i;

    i=j;

    j=t;

}//Swap

  한 줄이 길어지는 것은 보기에 좋지 않다. 그러므로 일반적으로 템플릿의
헤더 부분은 아래와 같이 적는다.

    template<class SwapType>

    void Swap(SwapType &i, SwapType &j) {

        SwapType t=i;

        i=j;

        j=t;

    }//Swap

  template<class SwapType>의 class란 말에 주의하라. 이것은 SwapType
이 클래스라는 것을 의미하는 것이 아니다. 아직 결정되지 않은, 즉, 사용자가
지정해야 하는 데이터 형(type)을 의미한다. 아마도 C++의 설계자는

template<type SwapType>

이라는 문법을 먼저 생각했을지도 모른다. 하지만, 겨우 template에만 쓰일 목
적으로 새로운 키워드 'type'을 추가하는 것은 큰 부담이었을 것이다. 그래서
class를 사용하기로 결심했을 것이다4).

  4) 이것은 순전히 필자의 생각이다. 하지만 실제로 최근에 개정된 C++ 문법은 template

    에서 class대신에 typename이란 키워드를 사용하는 것을 허락한다. 이것은 Visual C++

    4.2에서는 지원하지 않는다. 5.0 이후부터는 지원한다. C++ 빌더 1.0과 이후 버전에서는

    typename과 mutable, explicit등의 최신 C++ 키워드와 문법을 모두 지원한다. 자신이

    사용하고 있는 컴파일러의 도움말 기능을 참고하라. C++의 창시자인 Stroustrup의 최

    근 저서인 "The C++ Programming Language" 3판을 참고하면, 이러한 최신 정보를 모

    두 얻을 수 있다. 어제 - 1998년 5월 20일 - 서점에서 이 책이 출판된 것을 보았다.

  위의 Swap()은 함수가 아니라, 템플릿 함수임에 주의하자. 이것은 Swap()
에 대한 함수 호출이 불가능함을 의미한다. 템플릿 함수는 함수가 아니라, 함
수를 찍어내는 틀(template)이다. 하지만 아래의 소스는 훌륭히 컴파일 되어
실행된다. 결과 또한 3,2로 제대로 인쇄된다. Swap()에 대한 함수 호출이 일
어나지 않았단 말인가?

    #include <stdio.h>

    template<class SwapType>

    void Swap(SwapType &i, SwapType &j) {

        SwapType t=i;

        i=j;

        j=t;

    }//Swap

    void main() {

        int i=2,j=3;

        Swap(i,j);

        printf("%d,%d\n",i,j);

    }//main

  우리가 느끼지는 못하지만, main()의 Swap()은 템플릿 함수 Swap()을 호출
한 것이 아니라, 컴파일 시간에 자동으로 생성된 void Swap(int &, int &)를
호출한 것이다. 컴파일러는 컴파일 시간에 템플릿 함수에 대한 모든 호출의
형을 검사하여, 그 형에 맞는 함수를 자동으로 생성한다. 이것은 후에 설명할
템플릿 클래스에 대해서도 마찬가지이다. 만약,

    ...

    char a='a',char b='b';

    int i=2,j=3;

    Swap(a,b);

    Swap(i,j);

    ...

같은 소스가 있다면, 컴파일러는 char와 int의 두 개의 Swap()함수를 생성할
것이다. 컴파일러가 형을 어떻게 결정할지는 우리가 걱정하지 않아도 된다.
Swap()의 파라미터 a 혹은 i의 형을 컴파일러는 이미 알고 있다■.

  이것은 컴파일 시간에 결정된다. 컴파일러는 변수의 형을 참고하여, 그 형에 해당하는

    함수를 템플릿을 이용하여 찍어낸다. 위의 경우 Swap()을 호출할 때, Swap(int,char)형

    식으로 함수를 호출하면 어떻게 될 것인가? 이것은 표준 변환의 규칙에 맞도록 적당

    한 형식으로 변환(conversion)되어 함수가 만들어 진다. Swap(int,char)는 아마도

    Swap(int,int)형의 함수로 만들어 질 것이다. 하지만, Swap(int,char)처럼 사용하는 것은

    바람직한 일은 아니다. 왜 A를 만들어 놓고 B처럼 사용하는가? 아래의 바람직한 예를

    참고하라.

    ...

    int j=2;

    char c='a';

    ...

    Swap(j,(int)c);

    ...

  위에서는 형이 1개뿐이 함수의 예를 들었지만, 실제로 template는 형의 리
스트(type list)를 지정할 수 있다. 만약 함수가 서로 다른 3개의 형을 파라미
터로 받는다면,

    template<class T,class U,class V>

    void Func(T t,U u,V v) {

        ...

    }//Func

처럼 지정할 수 있을 것이다.

  명시적 지정(explicit specification)

  template의 파라미터로 변수(value)를 지정할 수도 있다. 아래의 예를 고려
해 보자.

#include <iostream.h>

#include <string.h>

template<class T,class U,int i>

void f(T x) {

    U buffer[i];

    strcpy(buffer,x);

    cout << buffer << endl;

}

void main() {

    f("hello world");//컴파일 시간 에러가 발생한다.

}

  위의 예에서 template 함수 f()의 템플릿 파라미터는 3개, f()의 파라미터는
1개이다. 템플릿 파라미터의 3번째 파라미터는 정수이다. 이것은 int i에 의해
지정된다. 즉 정수 i가 파라미터로 넘어가는 것이다.

  템플릿 파라미터가 함수 f()에서 모두 사용됨에도 불구하고, 위의 프로그램
은 컴파일 시간 에러가 발생한다. 함수를 호출하는 쪽에서 T,U와 i에 관한 정
보를 모두 확인할 수 없기 때문이다. 그러므로 명시적으로 T,U와 i에 관한 지
정을 해야 한다. main()은 다음처럼 수정되어야 한다.

...

void main() {

    f<char*, char, 20>("hello world");

}

  함수 f()가 위의 문법처럼 호출되면 T,U와 i가 각각 char*, char와 20으로
결정된다. 템플릿의 파라미터이므로 <>안에 명시한다.

  함수의 디폴트 특성은 템플릿의 경우에도 적용된다. 위의 예에서 i의 디폴
트 값을 100으로 설정하기 위해서 template을 다음처럼 정의할 수 있다.

...

template<class T,class U,int i=100>

void f(T x) {

...

  이 경우 i가 생략되면, i는 100이다. 아래의 수정된 예를 참고하라.

#include <iostream.h>

#include <string.h>

template<class T,class U,int i=100>

void f(T x) {

    U buffer[i];

    strcpy(buffer,x);

    cout << buffer << endl;

}

void main() {

    f<char*, char>("hello world");//i는 100으로 지정된다.

}

  클래스 템플릿(class template)

  이제 클래스에 관한 것을 알아보자. 여러 응용에 스택(stack)은 빈번히 사용
된다. 스택에 들어가는 데이터 형이 응용마다 바뀔 것이므로, 한 가지 형으로
고정하여 스택 클래스를 만든는 것은 좋은 방법이 아니다. 하지만, 먼저 정수
(integer)를 다루는 스택을 만들어 보자.

    class CStack {

        int *data;

        int size,sp;

      public:

        CStack(int s) { data=new int[size=s]; sp=0; }

        void Push(int d);

        int Pop();

    };//CStack

  위의 소스에서 굵게 표시한 부분은 반드시 int여야 한다. 만약 long을 다루
는 스택을 만들기를 원한다면, 위에서 굵게 표시한 int를 모두 long으로 바꾸
어야 할 것이다. 만약 어떤 T형을 다루는 스택을 원한다면, int를 모두 T로
바꾸어야 할 것이다.

    class CStack {

        T *data;

        int size,sp;

      public:

        CStack(int s) { data=new T[size=s]; sp=0; }

        void Push(T d);

        T Pop();

    };//CStack

  위의 소스는 T를 다루는 스택이다. template 클래스는 이제 거의 완성되었
다. 우리가 추가해야 할 부분은, 위의 템플릿 함수의 예에서처럼
template<...> 부분이다. 완성된 스택 템플릿은 아래와 같다.

    template<class T>

    class CStack {

        T *data;

        int size,sp;

      public:

        CStack(int s) { data=new T[size=s]; sp=0; }

        void Push(T d);

        T Pop();

    };//CStack

  이제 두개의 멤버 함수 Push()와 Pop()을 완성해 보자. 이것이 클래스 내부
에서 정의된다면 아래와 같이 작성할 수 있을 것이다.

    template<class T>

    class CStack {

        ...

        void Push(T d) {

            data[sp]=d;

            ++sp;

        }//Push

        ...

    };//CStack

  하지만 클래스의 외부에서 정의할 일이 훨씬 많다. 이 경우 template의 문
법은 까다로우므로 주의해서 외워두어야 한다. 정의한 두 개의 함수의 소스는
아래와 같다. 어디가 잘못되었는가?

        template<class T>

        void Push(T d) {

            ...

        }//Push

        template<class T>

        T Pop() {

            ...

        }//Pop

  Push()와 Pop()이 함수가 아니라, 멤버 함수라는 것을 주의하자. 이것은 범
위 해결사(scope resolver)가 필요함을 의미한다.

        template<class T>

        void CStack::Push(T d) {

            ...

        }//Push

        template<class T>

        T CStack::Pop() {

            ...

        }//Pop

  아직 위의 소스는 완전하지 않다. 각각의 멤버 함수가 사용하는 T가 어떤
형인지 어떻게 알 수 있는가? 이것은 사용자가 명시해야 한다. 함수의 경우와
는 달리 클래스가 사용하는 형은 컴파일 시간에 알려지지 않는다는 것에 주의
하라. 그러므로 원하는 형의 스택을 만들기 위해서, 프로그래머가 명시적으로
형을 적어주어야 한다. 예를 들면 정수 스택을 만들기 위해서는

CStack<int> s(100);

이라고 적는다. 이것은 크기가 100인 정수 스택을 생성한다. 그렇다면 사용자
가 적은 CStack<int>에서 형에 관한 정보는 클래스 이름에 따라오는 <int>
뿐이다. 그러므로 멤버 함수를 만들 때, 클래스 이름 뒤에 형이 무엇인지 명시
한다. 객체를 만들 때, 클래스 이름 뒤에 형을 명시했듯이, 멤버 함수를 정의
할 때도, 클래스 이름 뒤에 명시한다. 이것이 명시되고 완전히 구현된 소스는
아래와 같다.

        template<class T>

        void CStack<T>::Push(T d) {

            data[sp]=d; ++sp;5)

  5) data[sp++]=data;로 쓸 수도 있다.

        }//Push

        template<class T>

        T CStack<T>::Pop() {

            --sp; return data[sp];6)

  6) return data[--sp]; 로 쓸 수도 있다.

        }//Pop

아래의 예제를 참고하라. 출력 결과는

a

1

이다.

    #include <stdio.h>

    template<class T>

    class CStack {

        T *data;

        int size,sp;

      public:

        CStack(int s) { data=new T[size=s]; sp=0; }

        void Push(T d);

        T Pop();

    };//CStack

    template<class T>

    void CStack<T>::Push(T d) {

        data[sp]=d; ++sp;

    }//Push

    template<class T>

    T CStack<T>::Pop() {

        --sp; return data[sp];

    }//Pop

    void main() {

        CStack<char> s(10);

        CStack<int> t(10);

        s.Push('A');

        s.Push('a');

        t.Push(100);

        t.Push(1);

        printf("%c\n",s.Pop());

        printf("%d\n",t.Pop());

    }//main

  이제 완전한 template의 문법을 살펴보자. 문법은 아래와 같다.

template <type_list [, argument_list] >

이것은

template <class T, class U, int i, int j, char c>

같은 사용이 가능함을 의미한다. 독자들은 위에서 구현한 소스에서 sp와 data
의 형도 정의할 수 있기를 원할지도 모른다. 그렇다면 소스를 아래와 같이 수
정해야 한다. 고쳐진 부분은 굵은 글씨로 표기하였다.

    #include <stdio.h>

    template<class T,class TypeSP>

    class CStack {

        T *data;

        TypeSP size,sp;

      public:

        CStack(int s) { data=new T[size=s]; sp=0; }

        void Push(T d);

        T Pop();

    };//CStack

    template<class T,class TypeSP>

    void CStack<T,TypeSP>::Push(T d) {

        data[sp]=d; ++sp;

    }//Push

    template<class T,class TypeSP>

    T CStack<T,TypeSP>::Pop() {

        --sp; return data[sp];

    }//Pop

    void main() {

        CStack<char,int> s(10);

        CStack<int,int> t(10);

        s.Push('A');

        s.Push('a');

        t.Push(100);

        t.Push(1);

        printf("%c\n",s.Pop());

        printf("%d\n",t.Pop());

    }//main

  main()에서 int형의 스택 객체를 만들기 위해 CStack<int,int>라고 했음에
주목하라. 첫 번째 int는 스택에 Push/Pop되는 데이터가 int임을 의미한다. 두
번째 int는 sp와 size의 데이터 형이 int임을 의미한다.

  템플릿 클래스는 또한 정해진 상수가 필요할 지도 모른다. 같은 일을 하는
클래스라도 만들어서 사용하는 프로그래머의 의도에 따라 어떤 때는 10, 어떤
때는 5가 되는 상수 값이 있을 것이다. 이것은 template에서 두 번째 파라미
터 리스트로써 지정한다. 예를 들어 위에서 스택의 크기를 생성자에서 할당하
고 있는데, 이것은 template의 파라미터로 지정하는 것이 가능하다. 바뀌어진
소스는 아래와 같다.

    #include <stdio.h>

    template<class T,class TypeSP,int size=100>

    class CStack {

        T data[size];

        TypeSP sp;//size는 이제 필요 없다.

      public:

        CStack() { sp=0; }

        void Push(T d);

        T Pop();

    };//CStack

    template<class T,class TypeSP,int size>

    void CStack<T,TypeSP,size>::Push(T d) {

        data[sp]=d; ++sp;

    }//Push

    template<class T,class TypeSP,int size>

    T CStack<T,TypeSP,size>::Pop() {

        --sp; return data[sp];

    }//Pop

    void main() {

        CStack<char,int,10> s;//default constructor가 없으므로, 생성

                              //자의 파라미터를 명시하지 않는다.

        CStack<int,int> t;//size는 default 값 100이 사용된다.

        s.Push('A');

        s.Push('a');

        t.Push(100);

        t.Push(1);

        printf("%c\n",s.Pop());

        printf("%d\n",t.Pop());

    }//main

  위의 예에서는 스택의 크기를 template의 파라미터 size로 지정하며, 또한
디폴트 파라미터(default parameter)를 사용하고 있다. 디폴트 파라미터를 사
용하는 규칙을 잊어버리지는 않았는가? 선언하는 쪽에 한번만 디폴트 값을
명시해야 한다.

  template의 문법을 다룰 때, 마지막으로 주의 할 사항은 포인터에 관한 것
이다. template 클래스의 포인터 변수가 선언되었을 때, 혹은 template 클래스
를 상속받는 클래스에서 어떻게 template 클래스의 객체를 만드는가? 이것은
new의 사용과 밀접한 관계가 있다. 예를 들어 main()에서 선언된 변수가

CStack<int,int> s;

가 아니고,

CStack<int,int> *s;

라면, 어떻게 객체를 만드는가? 아래의 예제는 long형의 스택을, 포인터로 만
드는 예제다.

        ...

        CStack<long,long> *u;

        u=new CStack<long,long>;

        u->Push(100);

        u->Push(100);

        printf("%ld\n",u->Pop());

        delete u;

        ...

  만약 디폴트 생성자가 없어서, 파라미터를 지정해야 한다면,

        u=new CStack<long,long>(3);

처럼 쓸 수 있다. 이것은 매우 복잡해 보인다. 마지막으로 더 복잡한 template
의 예를 들고, 이 장의 이야기를 마치려 한다. 이야기하려는 주제는 template
의 파라미터가 template인 경우이다. 물론 이것은 가능하다. 또한 매우 유동적
이라서 표준을 만드는데 가끔씩 사용될 것이다7).

  7) ODMG(Object Database Management Group)에서 지정한 표준안에서 이것을 사용하

    고 있는 것을 본적이 있다.

#include <stdio.h>

template<class T,int s>

class A {

    T data[s];

  public:

    void Show(){}

};//class A

template<class Type,int b>

class B {

    Type buffer[b];

  public:

    void Show(){}

};//class B

void main() {

    B<A<int,5>,3> b;

}//main

  템플릿 클래스 B의 객체 b를 만들기 위해 지정한 첫 번째 파라미터는 템플
릿 클래스 A이다. 하지만, A 역시 템플릿 클래스이므로, 위와 같이

B<A<int,5>,3>

처럼 지정한다. B<<int,5>,3>이 불가능하다는 것을 주의하라. <int,5>에는 클
래스에 관한 아무런 정보도 없으므로, 반드시 A<int,5>처럼 사용해야 한다.

  이러한 템플릿이 얼마나 유용한지 다시 한번 강조한다. 만약 위에서 만든
스택 클래스에서 스택에 Push해야 할 데이터가 한번에 3개의 정수라면 어떻
게 구현할 것인가? 이것은 다음과 같이 구조체(structure)를 템플릿의 파라미
터로 지정함으로써 가능하다.

    struct Data {

        int i,j,k;

    };//struct Data

그러므로 객체를 만들 때,

CStack<Data> s;

처럼 만들어서 사용할 수 있다. 그리고 데이터를 Push하기 위해,

        ...

        CStack<Data> s;

        Data d={1,2,3};

        s.Push(d);

        ...

처럼 사용할 수 있을 것이다. 이렇게 사용하는 것이 싫다면, 다음과 같은 오버
로드된 Push()함수를 추가하면 될 것이다.

        void Push(int a,int b,int c) {

            data[sp].i=a;

            data[sp].j=b;

            data[sp].k=c;

            ++sp;

        }

이것을 응용한 완전한 소스를 아래에 리스트하였다.

    #include <stdio.h>

    struct Data {

        int i,j,k;

    };//struct Data

    template<class T,int size=100>

    class CStack {

        T data[size];

        int sp;

      public:

        CStack() { sp=0; }

        void Push(T d);

        T Pop();

        void Push(int a,int b,int c) {

            data[sp].i=a;

            data[sp].j=b;

            data[sp].k=c;

            ++sp;

        }//Push

    };//CStack

    template<class T,int size>

    void CStack<T,size>::Push(T d) {

        data[sp]=d; ++sp;

    }//Push

    template<class T,int size>

    T CStack<T,size>::Pop() {

        --sp; return data[sp];

    }//Pop

    void main() {

        CStack<Data> s;

        Data d={1,2,3};

        s.Push(d);

        s.Push(4,5,6);

    }//main

  typename

  여기서는 새로운 키워드 typename에 대해서 설명한다. 참고로 여기서 드는
예제는 C++빌더 1.0의 도움말에서 제공하는 예제이다. 하지만, Visual C++
5.0이후의 버전도 typename을 지원한다.

  typename의 용도는 2가지이다. 첫째, 단순히 template에서 class대신에 사용
할 수 있다. "The C++ Programming Language"의 저자이자 C++의 창시자
인 Bjarne Stroustrup은 typename보다 class를 선호한다고 한다. 이유는
typename은 8자지만 class는 5자이기 때문이다.

  이 장의 첫 부분에서 예로 든 Swap()함수는 아래와 같이 수정할 수 있다.

template<typename SwapType> void Swap(SwapType &i, SwapType &j) {

    SwapType t=i;

    i=j;

    j=t;

}//Swap

  둘째, template 클래스에서 template의 파라미터로 넘어온, 아직 알려지지
않은 클래스의 형을 참조하기 위해 사용한다. 예를 들면, 클래스 CTest에서
내부적으로 unsigned char 형을 A로 정의해서 사용한다고 하자.

class CTest {

    typedef unsigned char A;

};

  이 클래스가 어떤 template 클래스 혹은 template 함수의 파라미터로 넘어
간다고 가정하자. 또한, 그 클래스에서 파라미터로 넘어온 이 클래스의 내부에
서 정의한 형(type) A를 사용한다고 하자. template 클래스를 정의하는 입장
에서는 어떤 클래스가 넘어올지 모르기 때문에, 파라미터로 넘어온 그 클래스
의 내부에서 정의된 형의 변수를 선언하기 위해서는 특별한 키워드가 필요하
다. 이 때 typename을 사용한다. 아래의 예제를 참고하라.

//---------------------------------------------------------------------

#include <vcl\condefs.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <conio>

#include <iostream>

#pragma hdrstop

//---------------------------------------------------------------------

USERES("main.res");

//---------------------------------------------------------------------

template <class T>

void f(T i) {

  typedef typename T::A TA; // define TA as type T::A

  TA a5;		          	// declare a5 as type TA

  typename T::A a6;         // declare a6 as type T::A

  TA * pta6;             	// declare pta6 as pointer to type TA

}

class CTest {

  public:

    typedef unsigned char A;

    int i;

};

int main(int argc, char **argv)

{

    CTest t;

    f(t);

    getch();

    return 0;

}

//---------------------------------------------------------------------

  위의 예제에서 template 함수 f()는 파라미터로 넘어온 형의 내부에서 정의
된 A형의 변수 a6를 선언하기 위해,

typename T::A a6;

처럼 사용하고 있다. 독자들은 이 문장에 의문을 가지고 있는가? 왜

T::A a6;

처럼 변수 a6을 선언하는 것은 허락되지 않는가? 이것은 가능하지 않다. 하지
만, 이러한 선언이 타당하기 위해서는 T::A가 컴파일 시간에 알려져야 한다.
그러므로, main()에서

CTest::A b;

처럼 선언하는 것은 가능하며, 이것은 unsigned char형의 변수 b를 선언한다.
이것은 컴파일러가 CTest::A가 형이라는 정보를 컴파일 시간에 알 수 있으므
로, 가능한 것이다. 하지만, template로 넘어온 클래스의 형 A를 참조하기 위
해, T::A를 사용하는 것은 애매하다. T::A가 변수를 참조하는지, 형을 참조하
는지 어떻게 알 수 있는가? 그러므로 A가 형이라는 것을 알려주기 위해

typename T::A a6;

처럼 사용해야 하는 것이다. 이것이 template에서 파라미터 클래스의 내부에
서 정의된 형의 변수를 선언하기 위해서 typename을 사용해야 하는 이유이
다.

  실습문제

1.


46. 연산자 오버로딩(operator overloading)

46. 연산자 오버로딩
(operator overloading)

  이제 C++의 가장 긴 예제들 중의 하나를 접할 시점이다. 이것은 오버로딩
(overloading)과 관계된 것이다. 오버로딩이 짧게 설명될 수 있음에도 불구하
고, 우리는 모든 종류의 연산자에 관한 오버로딩을 살펴 볼 것이므로, 내용은
장황할(long and boring) 것이다. 하지만, 이 책을 완독(complete reading)한
후의 참조(reference)를 위해서 장황함을 감수해야 한다.

   더 유용한 도구: 오버로딩

  2개의 스트링을 더하는 것이 가능한가? 좀 더 엄격히 말하면, 2개의 char*
를 더하는 것이 가능한가? 아래의 예를 보자.

s="hello" + "world";

  Pascal 같은 언어에서는 이것이 가능함에도 불구하고, C++에서는 이것이 불
가능하다. 하지만, 이것을 가능하게 만드는 기능을 C++에서 제공한다. 이것은
C++의 연산자 오버로딩(operator overloading)에 의해서 가능하다.

  위의 예에서처럼, 스트링에 관한 연산자를 오버로딩하는 예가 적절함에도
불구하고, 필자는 먼저 복수소(complex number)에 관한 예를 다룰 것이다. 독
자들은 '생성자,소멸자'에서 이해하기 어려웠던(아직 이해하지 못할 수도 있
는) 부분인 복사 생성자(copy constructor)에 대한 기억을 상기하기 바란다.
어떤 독자는 이 부분을 이해하기 위해, 먼저 이 장을 읽고 있을지도 모른다.
그래서, 필자는 먼저 복소수에 관한 예를 다룬다. 고등학교에서 배웠던, 복소
수에 관한 기억을 상기해 보자.

  복소수는

a+bi

형태로 표현한다. 여기서 a,b는 실수이며(각각을 실수부, 허수부라 한다), i는
우리가 살고 있는 4차원 세계에서는 실제로 존재하지 않는, 제곱하면 -1이 되
는(i2=-1) 가상의 수(imaginary number)이며, 허수라고 한다. 즉 i=

sqrt -1

이다.
복소수가 컴퓨터 과학 분야에서 응용하는 곳이 종종 있음에도 불구하고, 우리
는 여기서 복소수 그 자체에 관해서는 깊게 다루지 않을 것이다.

  2개의 복소수 x,y를 각각 a+bi, c+di라 하자. 복소수에 관한 덧셈과 뺄셈은
실수부끼리 허수부끼리 가감하는 것이다. 그러므로,

x+y=(a+c)+(b+d)i

이며,

x-y=(a-c)+(b-d)i

이다. 자 이제 복소수를 표현하기 위해 CComplex 클래스를 만들어 보자. 이
클래스는 복소수의 덤셈과 뺄셈을 지원하기 위해, Add(), Subtract() 멤버 함
수를 제공한다.

#include <stdio.h>

class CComplex {

    float a,b;

public:

    CComplex(float ta,float tb=0) {

        a=ta; b=tb;

    }

    void Print();

    void Add(CComplex c);//함수를 이렇게 작성하는 것은 좋지 않다. 무엇이

                         //문제인가?

    void Subtract(CComplex c);

};//class CComplex

void CComplex::Print() {

    printf("%2.2f%+2.2fi\n",a,b);

      //          ^부호를 출력한다.

}

void CComplex::Add(CComplex c) {

    a+=c.a; b+=c.b;

}

void CComplex::Subtract(CComplex c) {

    a-=c.a; b-=c.b;

}

void main() {

    CComplex x(1.,2.),y(3.,4.);

    x.Print();

    x.Add(y);//수학적으로 x=x+y의 의미이다.

    x.Print();

}

  실행 결과는

1.00+2.00i

4.00+6.00i

이다.

  자 위의 예에서 main()을 주목해 보자. 복소수 x에 복소수 y를 더하기 위
해,

x.Add(y);

를 호출한다. 실제 이것의 수학적 표현은

x=x+y;

이다. 하지만, C++에서 이런 표현은 불가능해 보인다. 하지만, 이런 표현이 가
능하도록 CComplex를 수정할 수 있다. 어떤 독자들은 x=x+y라는 표현 자체
를 싫어할지 모른다.

'나는 x.Add(y) 라는 형태가 훨 좋아!'

그러한 독자들은 이 장의 나머지 부분을 무시해도 좋다. 오버로딩으로 구현하
더라도, 실제 x.Add(y)와 x=x+y 라는 표현은 '표현 자체의 차이' 외에 연산
속도 등의 차이는 없다. 하지만, x=x+y라는 표현은 프로그램을 더 읽기 좋게
만든다. 즉, 힘(power)은 같지만, 표현 자체가 엄청난 차이를 가지는 것이다!

왜 에러인가?

  위의 소스에서

x.Add(y);

를

x=x+y;

로 바꾸어 컴파일 해보자. 에러가 발생한다. 이 문장이 왜 에러인가? 그것은
+연산자의 기능 때문이다. +연산자는 int, float같은 같은(same) 기본형(basic
type)의 덧셈에 대해서만 동작하기 때문이다. 예를 들면,

    int a=1;

    float f=0;

    f=f+a;

는 엄격한 의미에서 잘못이다. 비록 f와 a가 기본형이기는 하지만, 형이 다르
기 때문이다. 하지만, C++는 이 문장을 에러 없이 컴파일하는데, 그것은 덧셈
전에 a를 float형으로 형 변환(casting)하기 때문이다. 그러므로, x=x+y 는 분
명한 에러이다. x,y는 기본형이 아니기 때문이다.

  독자들은 함수 오버로딩(function name overloading)을 기억하고 있을
것이다. 그것은 같은 이름의 함수에 대해, 함수를 호출하는 쪽의 문맥(context:
함수의 경우, 파라미터의 개수와 형의 종류)으로 구분이 가능하면, 같은 이름
의 함수를 여러 개 작성할 수 있는 기능이었다.

  지금도 상황이 비슷하다. 비록 +가 기본형의 덧셈으로 정의된 연산자이지
만, +의 문맥(+좌우의 2개의 형)으로 다른 +의 기능이 수행되도록 할 수 없을
까? 즉, 우리가 CComplex 클래스의 +를 정의한 다음, 우리가 정의한 + 기능
이 호출되도록 하는 것이다. 이것을 연산자 오버로딩(operator overloading)
이라 한다.

   새로운 키워드 operator

  C++의 새로운 키워드 operator를 사용하여 이미 존재하는 대부분의 연산자
의 기능을 새로 정의하는 것이 가능하다. 이것은 어려운 작업이 아니다. 예를
들어 CComplex에 연산자 함수(operator function) +를 정의해 보자. 독자들
은 왜 이것을 '연산자 +'라 하지 않고, '연산자 함수 +'라고 하는지 곧 알게 될
것이다.

  먼저 연산자 함수 +의 리턴 형을

void

라고 하자. 그리고 함수의 이름은

operator +■

             이것은 얼마나 이상한 함수 이름인가? 연산자 함수는 operator라고 명시
          한 뒤에 반드시 오버로딩 가능한 연산자가 위치해야 한다. 물론 연산자 자
          체가 토큰을 구분하는 특수문자로 동작하므로, operator+처럼 붙여써도 무
          방하다.

라 하자. 그리고, 이 함수의 파라미터는

(CComplex c)

이다. 그러면 연산자 함수 +의 선언은 다음과 같은 모양이 될 것이다.

void operator +(CComplex c);

  와우! 이것은 굉장히 이상한 모양의 함수같다. 하지만, 이 형태는

void Add(CComplex c);

와 동일하다. Add대신에 operator +를 쓴 것뿐이다. 이제 이 연산자 함수를
추가한 CComplex를 써 보면 소스는 아래와 같다.

#include <stdio.h>

class CComplex {

    float a,b;

public:

    CComplex(float ta,float tb=0) {

        a=ta; b=tb;

    }

    void Print();

    void operator +(CComplex c);//Add 대신 operator+ 사용

    void Subtract(CComplex c);

};//class CComplex

void CComplex::Print() {

    printf("%2.2f%+2.2fi\n",a,b);

}

void CComplex::operator +(CComplex c) {//Add 대신 operator+ 사용

    a+=c.a; b+=c.b;

}

void CComplex::Subtract(CComplex c) {

    a-=c.a; b-=c.b;

}

void main() {

    CComplex x(1.,2.),y(3.,4.);

    x.Print();

    x.operator +(y);//Add 대신 operator+ 사용

    x.Print();

}

  위의 소스를 자세히 보면, 바뀐 부분은

Add

대신에

operator+

를 사용한 것뿐이다. 연산자 함수 +에 대한 호출인

x.operator+(y);

를 명시적 호출(explicit call)이라 한다. 실제로 명시적 호출은 Add로 정의
했을 때의 Add() 호출과 같다. 또한, 연산자 함수가 정의되었을 때, 명시적 호
출은 무의미해 보인다(이것은 Add()보다 읽기 어렵다!).

  자 이제 명시적 호출을 함축적 호출(implicit call)로 바꾸어 보자.

x.operator+(y);

를

x+y;

로 바꾼다. 와우! 그래도 결과는 같다. 아래에 main()만 리스트하였다.
CComplex는 그대로이다.

...

void main() {

    CComplex x(1.,2.),y(3.,4.);

    x.Print();

    x+y;//x.operator +(y);

    x.Print();

}

   컴파일러의 입장: 나 피곤해요(^_^)

  위의 예에서

x+y;

는 실제로 컴파일러에 의해 어떻게 해석되는 것일까? 그것도 컴파일 시간
(compile time)에!

  초기에 C++의 연산자 오버로딩을 설계했던 사람(그는 Stroustrup이 아니었
다)은 천재였음에 틀림없다!

  x+y는 다음과 같이 해석된다.

① 컴파일러는 + 연산자가 이항 연산자(binary operator)라는 것을 알고 있으
며, x+y; 를 만났을 때, x,y의 형도 이미 알고 있는 상태이다.

② 컴파일러는 + 왼쪽의 형을 검사하여, 그 형이 기본형이 아니면, 그 형에
정의된 연산자 멤버 함수가 있는지 검사한다. 즉 x의 형이 CComplex이므로,
CComplex에 operator+가 정의되어 있는지를 검사한다. 만약 정의되어 있지
않으면 컴파일러는 에러 메시지를 출력한다.

③ 연산자 함수 operator+가 정의되어 있으면, x+는 x.operator+()에 대한 호
출로 번역되며, + 오른쪽의 y는 이 함수의 첫 번째 파라미터가 된다. 그러므
로, x.operator+(y)로 해석하게 되는 것이다. 물론 이 때 y의 형도 검사한다.

  자, CComplex 클래스에 정의된 연산자 함수 +에 대해 다음의 문장들은 왜
에러인가?

x+3;

x-y;

  x+3은 컴파일러가 x.operator+(3)으로 해석을 시도할 것이다. 하지만,
operator+함수의 파라미터는 정수(integer)가 아니다.

  x-3은 컴파일러가 x.operator-(y)로 해석을 시도할 것이다. 하지만,
operator-는 정의되어 있지 않다.

  자 아래의 문장은 왜 또 에러인가?

x=x+y;

컴파일러는 위 문장을

x=x.operator+(y);

로 해석을 시도한다. 하지만, operator+의 리턴 값은 void이므로 에러가 되는
것이다. 이러한 문제를 모두 해결해 보자. 즉, operator+는 정수도 파라미터로
받아야 하며, operator-를 정의해야 하고, 또한, operator+의 리턴 값은
CComplex 형이 되어야 한다.

  먼저 operator+가 CComplex를 리턴하도록 수정해 보자. 함수의 선언을

CComplex operator+(CComplex c);

로 바꾼다. 함수의 정의는 다음처럼 될 것이다. 임시 객체 t를 사용하지 않으
면 발생하는 문제점에 대해서는 실습문제 1을 참고하라.

CComplex CComplex::operator+(CComplex c) {

    CComplex t(a,b);

    t.a+=c.a; t.b+=c.b;

    return t;

}

  위 함수는 (스택에 할당된) 임시 객체 t를 만들어, 먼저 자신의 값으로 초기
화한 다음, c를 더해 t를 리턴한다.

  x+3의 의미를 '복소수 x의 실수부에 3을 더하는 것'으로 정의하면, 다음과
같이 '함수 이름 오버로딩'을 적용하여 두 번째 operator+를 정의할 수 있다.

CComplex CComplex::operator+(float r) {

    CComplex t(a,b);

    t.a+=r;

    return t;

}

  뺄셈에 대한 operator-는 독자들이 직접 정의해 보기 바란다.

완성된 소스를 아래에 리스트하였다.

#include <stdio.h>

class CComplex {

    float a,b;

public:

    CComplex(float ta,float tb=0) {

        a=ta; b=tb;

    }

    void Print();

    CComplex operator+(CComplex c);

    CComplex operator+(float r);

    CComplex operator-(CComplex c);

};//class CComplex

void CComplex::Print() {

    printf("%2.2f%+2.2fi\n",a,b);

}

CComplex CComplex::operator+(CComplex c) {

    CComplex t(a,b);

    t.a+=c.a; t.b+=c.b;

    return t;

}

CComplex CComplex::operator+(float r) {

    CComplex t(a,b);

    t.a+=r;

    return t;

}

CComplex CComplex::operator-(CComplex c) {

    CComplex t(a,b);

    t.a-=c.a; t.b-=c.b;

    return t;

}

void main() {

    CComplex x(1.,2.),y(3.,4.);

    x.Print();

    x=x+y;//x=x.operator+(y);로 해석⼆한다.

             물론, x=x+y; 를 x=x.operator+(y); 로 명시적 호출을 사용해도 에러가 아니
          다.

    x.Print();

    y=x-y;//y=x.operator-(y);로 해석한다.

    y.Print();

}

결과는

1.00+2.00i

4.00+6.00i

1.00+2.00i

이다. 결론은 다음과 같다.

"연산자 오버로딩의 함축적 호출은 컴파일 시간에 명시적 호출로 변환된
후 번역된다(컴파일러는 피곤하다)."

   복사 생성자(copy constructor)

  실제로 위에서 정의한

CComplex operator+(CComplex c);

등의 연산자 함수는,

CComplex& operator+(CComplex& c);

로 정의하는 것이 바람직하다. 이유는 '생성자,소멸자'의 복사 생성자(copy
constructor)를 참고하라. 연산자 오버로딩에 관한 조언은 다음과 같다.

"연산자 오버로딩 함수의 파라미터는 반드시 레퍼런스를 전달하라. 또한
반드시 레퍼런스를 리턴하라."

명시적 호출(explicit call)■

             직접 호출(direct call)이라고도 한다.

  함축적 호출이 명시적 호출로 변환되어 컴파일 되므로, 직접적으로 명시적
호출을 사용하는 것이 가능하다.

cout << "hello" << i;

의 명시적 호출은

(cout.operator<<("hello")).operator<<(i)

이다. 지금 당장 확인해 보라.

   불가능의 이유: 친구가 필요하다!

  연산자 오버로딩 함수를

CComplex operator+(CComplex c,CComplex d);

처럼 정의하는 것은 가능할까? 이것은 불가능하다. 이것은 함축적 호출의 변
환 과정을 생각해 보면 명백하다.

  예를 들어, x+y는 x.operator+(y)로 번역되므로, operator+ 함수의 함축적 호
출에 2개의 파라미터를 명시하는 것은 불가능하다. 만약 위의 연산자 함수가
정의된다면, x.operator+(y,z)를 호출하기 위해서는

x+y,z

처럼 명시해야 할 것이다. 하지만, 이것은 문법적 에러이다.

  어려움이 있을 때, 친구(friend)는 항상 도움을 주기 마련이다. x+y의 변
환을 CComplex의 연산자 멤버 함수

x.operator+(y)

가 아닌, 일반 연산자 함수

operator+(x,y)

로 변환하도록 하는 방법은 없을까? 이러한 변환이 가능하다면, operator+는
더 이상 특정 클래스의 멤버 함수가 아니라, 일반 함수(non member function)
이다.

  자 그러면, 일반 함수 operator+를 다음과 같이 정의해 보자.

CComplex operator+(CComplex x,CComplex y) {

    CComplex t(x.a,x.b);

    t.a+=y.a; t.b+=y.b;

    return t;

}

  위의 정의에서 operator+는 더 이상 특정 클래스의 멤버 함수가 아니라, 일
반 함수이다. 이 함수는 두 개의 CComplex 객체를 파라미터로 받아 더한
(add) 결과의 복소수 객체를 리턴한다. 하지만, 위의 함수를 사용하기 위해서
는 CComplex의 소스 중 private: 에 선언된

float a,b;

의 접근 권한을 public: 으로 변경해야 한다. 소스를 아래에 리스트하였다. 아
래의 소스에서는 설명을 위해 이미 설명된, 필요 없는 부분은 제거하였다.

#include <stdio.h>

class CComplex {

public:

    float a,b;//a,b는 외부 함수 operator+에서 볼 수 있어야하므로,

              //public:이어야 한다.

public:

    CComplex(float ta=0,float tb=0) {

        a=ta; b=tb;

    }

    void Print();

};//class CComplex

void CComplex::Print() {

    printf("%2.2f%+2.2fi\n",a,b);

}

CComplex operator+(CComplex x,CComplex y) {

  //이제 operator+는 CComplex의 멤버 함수가 아니다.

    CComplex t(x.a,x.b);

    t.a+=y.a; t.b+=y.b;

    return t;

}

void main() {

    CComplex x(1.,2.),y(3.,4.),z;

    z=x+y;

    z.Print();

}

  결과는

4.00+6.00i

이다. 위의 예에서 operator+는 더 이상 CComplex의 멤버 함수가 아니다. 그
렇다면, z=x+y; 를 컴파일러가 어떻게 해석하는 것일까?

  ① 컴파일러는 +의 왼쪽과 오른쪽의 형이 기본형이 아닌 경우 일반 함수
operator+가 있는지 검사한다.

  ② 있다면 x+y에 대해, operator+(x,y)의 호출이 가능한지 검사한다. 가능하
지 않다면 에러 메시지를 출력한다.

  ③ 그리고, operator+의 리턴 값을 =의 왼쪽에 대입할 수 있는지 검사한다.
즉 z=x+y 는 z=operator+(x,y)로 번역하는 것이다.

  만약 멤버 연산자 함수와 일반 연산자 함수가 둘 다 존재하는 경우, 컴파일
러는 무엇을 먼저 적용할까? 즉 아래의 결과는 어떻게 될까?

//이 소스는 컴파일 에러가 발생한다.

#include <stdio.h>

class CComplex {

public:

    float a,b;

public:

    CComplex(float ta=0,float tb=0) {

        a=ta; b=tb;

    }

    void Print();

    CComplex operator+(CComplex x);

};//class CComplex

void CComplex::Print() {

    printf("%2.2f%+2.2fi\n",a,b);

}

CComplex CComplex::operator+(CComplex x) {

    CComplex t(a,b);

    t.a+=x.a; t.b+=x.b;

    return t;

}

CComplex operator+(CComplex x,CComplex y) {

    CComplex t(x.a,x.b);

    t.a+=y.a; t.b+=y.b;

    return t;

}

void main() {

    CComplex x(1.,2.),y(3.,4.),z;

    z=x+y;

    z.Print();

}

  위의 소스는 컴파일되지 않는다. 에러 메시지는

'operator +' is ambiguous

이다. 즉, 컴파일러는 혼돈의 여지가 있는 어떠한 오버로딩도 허용하지 않는
다.

  일반 함수 operator+를 사용한 위의 소스는 아직까지는 완벽하지 않다. 실
수부와 허수부가 private: 여야 함에도 불구하고, operator+를 위해 public: 으
로 선언한 것이다. 이것은 분명 설계상의 잘못이다!

  private 멤버를 접근(access)하는 방법을 독자들은 잊지 않고 기억하고 있을
것이다. 바로 친구 기능을 사용하는 것이다. 수정된 소스는 아래와 같다.

#include <stdio.h>

class CComplex {

private://이제 private로 다시 선언한다.

    float a,b;

public:

    CComplex(float ta=0,float tb=0) {

        a=ta; b=tb;

    }

    void Print();

    friend CComplex operator+(CComplex x,CComplex y);

      //친구는 나의 모든 것을 접근할 수 있다. 애인만 빼고...

};//class CComplex

void CComplex::Print() {

    printf("%2.2f%+2.2fi\n",a,b);

}

CComplex operator+(CComplex x,CComplex y) {

//이제 operator+에서 CComplex의 private 멤버를 접근하는 것이 허용된

//다.

//CComplex는 이 함수를 친구로 생각하고 있는 것이다.

    CComplex t(x.a,x.b);

    t.a+=y.a; t.b+=y.b;

    return t;

}

void main() {

    CComplex x(1.,2.),y(3.,4.),z;

    z=x+y;

    z.Print();

}

  일반 함수로 연산자 오버로딩을 하는 조언은 다음과 같다.

"연산자 오버로딩은 반드시 친구 함수로 구현하라."

  위의 예에서 든, friend operator+는 사실 적절한 것이 아니었다. 예를 들면,

x=3+y;

를 컴파일하기 위해서는

x=3.operator+(y);

으로 해석하는 것이 불가능하므로(3같은 정수에 대해서는 연산자 오버로딩할
수 없다), 반드시 friend를 사용하여 operator+를 오버로딩해야 한다. 아래의
소스를 보라!

#include <stdio.h>

class CComplex {

private:

    float a,b;

public:

    CComplex(float ta=0,float tb=0) {

        a=ta; b=tb;

    }

    void Print();

    friend CComplex operator+(CComplex x,CComplex y);

    friend CComplex operator+(float r,CComplex y);

};//class CComplex

void CComplex::Print() {

    printf("%2.2f%+2.2fi\n",a,b);

}

CComplex operator+(CComplex x,CComplex y) {

    CComplex t(x.a,x.b);

    t.a+=y.a; t.b+=y.b;

    return t;

}

CComplex operator+(float r,CComplex y) {

    CComplex t(r);

    t.a+=y.a; t.b+=y.b;

    return t;

}

void main() {

    CComplex x(1.,2.),y(3.,4.),z;

    z=3+y;

    z.Print();

}

결과는

6.00+4.00i

이다. 자 연산자 오버로딩의 규칙을 외워 두자.

"대개의 경우 멤버 함수로 연산자 오버로딩을 하라. 멤버 함수로 불가능한
경우에 대해서만, 반드시 친구 함수로 연산자 오버로딩을 하라."

오버로딩 할 수 없는 연산자

  삼항 연산자(ternary operator)

?:

은 오버로딩할 수 없다. 또한

::  .  .*

도 오버로딩할 수 없으며, 전처리 명령에서만 사용하는 연산자인

# ##

도 오버로딩할 수 없다. 점 연산자(dot operator) . 은 오버로딩할 수 없지만,

->

는 오버로딩할 수 있는 것은 흥미롭다.

   형 변환 연산자(type conversion operator)

  형 변환(casting)에 대해서도, 사용자가 기능을 정의할 수 있다. 이것은 넓
게 연산자 오버로딩의 범주에 속하는 것이지만, 분명 다르다. 아래의 소스를
보자.

#include <iostream.h>

#include <stdio.h>

#include <conio.h>

#define HIBYTE(i) ((i)>>8)

#define LOBYTE(i) ((i)&0x00ff)

class test {

    char h,l;

  public:

    test(char a,char b) { h=a; l=b; }

    test() { h=l=0; }

    char gethigh() { return h; }

    char getlow() { return l; }

    operator int();//리턴 형은 명시할 수 없다.

};

test::operator int()

{

    int i=h;

    i<<=8; i|=l;

    return i;

}

void main()

{

    test i(1,2);

    int j;

    j=i;

    cout << hex << HIBYTE(j) << LOBYTE(j) << endl;

}

  결과는

12

이다. 위의 문장에서

j=i;

는 컴파일러에 의해, i의 왼쪽 변수 j의 형이 int인 것을 알 수 있으므로,

j=i.int();

로 번역된다. 하지만, 형 변환 연산자에 대해 이러한 명시적 호출은 불가능하
다.

  형 변환 연산자는 매우 강력하다. 예를 들면, MFC(Microsoft Foundation
Class)의 표준 클래스인 CDC에서 CDC의 멤버인 m_hdc를 HDC로 형 변환하
는데 형 변환 연산자를 사용하고 있다. C++의 형 변환 연산자를 모르는 MFC
사용자에게 이것은 매우 이상하게 동작하는 것처럼 보일 것이다. 진리는 우리
를 자유롭게 한다.

사용자 정의 형 변환 함수(user defined conversion
operator)

  MFC의 CDC의 형 변환 함수 operator HDC()처럼 사용자가 정의한 형에
대해서도(심지어 클래스도) 형 변환 함수를 작성할 수 있다. 이것은 당연한
것이다. 아래의 예를 참고하라.

#include <iostream.h>

typedef unsigned char BYTE;

class CTest {

    int i;

public:

    CTest() { i=0x0102030405060708; }

    operator BYTE() {

        return (BYTE)((i&0x00000000000000ff00) >> 8);

    }

};//class CTest

void main() {

    CTest t;

    BYTE b=t;//형 변환 함수 BYTE()를 호출한다.

    cout << (int)b << endl;

}

결과는

7

이다.

   다른 예들

  이제 다른 연산자에 대한 오버로딩의 예를 살펴보자. 독자들은 오버로딩할
수 있는 모든 연산자를 살펴보기 위해, '연산자'를 참고할 수 있다. 위에서 제
시한 '오버로딩할 수 없는 6개의 연산자(?:, ::, ., .*, #, ##)'를 제외하고, 다른
모든 연산자들은 오버로딩할 수 있다!

  처음의 약속과는 달리, 모든 연산자에 대한 예를 제시하지 않는다. 하지만,
독자들은 이 문제를 실습문제에서 찾아 볼 수 있다. 모든 연산자에 대한 오버
로딩에서 주의 사항이 있다.

"연산자 오버로딩이 연산자의 기능을 바꿀 수는 있지만, 연산자 우선
순위(priority) 및 피연산자의 개수 특성을 바꿀 수는 없다. 즉, +연산자는
이항 연산자(binary operator)이므로, 피연산자를 1개 가지도록
오버로딩할 수는 없다."

  이러한 특성은 사실 당연한 것이다. 우선순위 및 피연산자의 개수 특성을
문법적으로 표현하기 위해 C++의 문법을 수정하는 것은, 그렇지 않은 경우에
비해 치뤄야 할 대가가 너무 크다.

[]의 오버로딩: 참조의 리턴

  아래의 예는 []를 오버로딩한 예이다. CTest는 동적으로 정수형 배열을 유
지한다. 동적으로 만들어진 배열의 요소(element)를 참조하기 위해 operator[]
를 사용한다.

#include <iostream.h>

class CTest {

    int* ip;

    int size;

public:

    CTest(int s) {

        ip=new int[s];

        size=s;

    }

    ~CTest() {

        delete[] ip;

        size=0;

    }

    int GetSize() { return size; }

    void SetData(int i,int data) {

        ip[i]=data;

          //*(ip+i)=data와 같다는 것을 독자들은 잊지 않고 있는가?

    }

    int operator[](int i);

};//class CTest

int CTest::operator[](int i) {

    return ip[i];

}//CTest::operator[]

void main() {

    CTest a(100);

    a.SetData(0,31);

    cout << a.operator[](0) << endl;

      //    ^a[0]으로 적어도 좋다.

}

  결과는

31

이다. 물론 위의 소스에서 main()의

cout << a.operator[](0) << endl;

을

cout << a[0] << endl;

로 적는 것이 바람직하다. 하지만, 필자는 다시 한번 명시적 호출을 강조하기
위해, 명시적 호출로 main()의 소스를 작성해 두었다.

  위의 main()에서

SetData(0,31);

을

a[0]=31;

로 고치면 어떻게 될까? 자 고쳐서 실행해 보자.

  결과는

"left operand must be l-value"

라는 에러 메시지를 출력한다. 예전에 설명했던 L-value(left value)의 악몽(?)
이 다시 나타난다. 등혹(=)의 왼쪽에는 반드시 포인터로 번역될 수 있는 표현,
즉 L-value가 와야함을 상기하자.

a[0]=31;

에 대해 컴파일러는

a.operator[](0)=31;

로 번역을 시도할 것이다. operator[]의 리턴값은 정수형(int)이므로, 위 문장은

0=31;

에서 범한 오류와 같은 오류를 범하고 있다. 즉 L-value 자리에 상수
(constant)가 위치한 것이다!

  함수가 리턴하는 값을 바꾸는 기능에 대해서 기억하고 있는가? 그것은 바
로 참조(reference)를 리턴하는 함수였다. 위의 경우

int operator[](int i);

를

int& operator[](int i);

로 바꾼다. 물론 함수의 정의부분(definition part)도 수정한다. 바뀐 소스를 아
래에 리스트하였다.

#include <iostream.h>

class CTest {

    int* ip;

    int size;

public:

    CTest(int s) {

        ip=new int[s];

        size=s;

    }

    ~CTest() {

        delete[] ip;

        size=0;

    }

    int GetSize() { return size; }

    void SetData(int i,int data) {

        ip[i]=data;

    }

    int& operator[](int i);

};//class CTest

int& CTest::operator[](int i) {

    return ip[i];

}//CTest::operator[]

void main() {

    CTest a(100);

    //a.SetData(0,31);

    a[0]=31;//이제 a[0]은 L-value가 될 수 있다.

    cout << a[0] << endl;

}

  결과는

31

이다. 참조를 리턴하는 함수에 대한 기억이 희미한 독자는 '참조(reference)'
를 참고하라.

  실제로 연산자는 대부분 복합적으로 사용되므로, 연산자의 리턴값을 참조로
구현하는 것은 일반적인 것이다. 예를 들면,

cout << "hello" << "world" << endl;

과 같은 iostream에 존재하는 operator<<에 대해, 만약 참조를 리턴하도록 구
현하지 않았다면, 위와 같은 사용은 불가능하다(반드시 iostream.h의
operator<<를 찾아 보라).

()의 오버로딩

   new와 delete의 오버로딩

  실습문제

1. 복소수 클래스 CComplex의 연산자 함수 +를 아래와 같이 정의하는 것은
왜 논리적 오류인가?(힌트: side effect)

CComplex CComplex::operator+(CComplex c) {

    a+=c.a; b+=c.b;

    return *this;

}

2. 형 변환 함수(type conversion operator)에서 리턴 형은 왜 불필요한가?

3. '연산자'에서 제시한 모든 연산자에 대해 오버로딩을 구현하라. 오버로딩을
위한 클래스는 독자들 임의로 선택하라.

4. 동적으로 정수형 배열을 할당하여 유지하는 CTest에는 operator[]가 정의되
어 있다. operator[]를 수정하여 첨자(index)가 0이 아닌, 1부터 시작하도록
operator[]를 수정하라. 즉, 크기가 100인 경우, 첨자는 0∼99가 아닌, 1∼100이
되어야 한다.

5.


48. Win32 메모리 모델

  (qff)

   실습문제

1.


일반 페이지 (제목 없음)

 멤버변수(Member Variable)

멤버변수의 포인터와 정적(static) 멤버변수에 관한 설명입니다. 아래와 같은
Class를 생각해 봅시다.

class CTest {

    int a,b,c;

  public:

    CTest() { a=b=c=0; }

    void SetValue(CTest& t,int i);

    void Print() { printf("%d,%d,%d\n",a,b,c); }

};

위의 CTest 클래스는 int형 멤버변수 3개를 가집니다.

Constructor는 3개의 member variable를 0으로 초기화시며, Print()는 값을 출력하고,
SetValue()는 파라미터로 넘어간 CTest 클래스의 객체 t의 멤버면수 a를 i로 초기화
시킵니다. SetValue()가 멤버함수일 필요가 있느냐에 관해서는 의문이 생기는 분이
계실 건데요. 일단 그 문제는 보류하기로 하죠.

SetValue()를 아래와 같이 구현했습니다. 가능합니까?

void CTest::SetValue(CTest& t,int i)

{

    int *ip;

    ip=&(t.a);

    t.*ip=i;

}//CTest::SetValue

그러니까 제작자의 의도는 CTest의 멤버변수 a를 가리키는 int형 포인터를
선언해서 t.a의 주소를 받아와서 ip를 이용해서 a를 초기화할려고 한 것입니다.

일반적인 C의 예를 들어보면,

int i=100;

void SetValue(int d)

{

    int *ip;

    ip=&i;

    *ip=d;

}

위의 C 프로그램은 훌륭하게 잘 동작합니다. 하지만 CTest::SetValue()는 잘못
구현된 것입니다. 사용자는 C++의 다음 사항을 모르고 있는 것 같습니다.

"변수(variable)와 멤버변수(member variable)는 같지 않다."

그러니까,

ip=&(t.a);

문장에서 "type mismatch" 에러가 발생하겠죠? &t.a는 정수형 멤버변수에 관한
포인터이지 정수 포인터가 아닙니다. 그래서 ip의 선언을

int CTest::*ip;

처럼해야 합니다. 이 선언은 ip가 CTest의 정수형 멤버변수에 관한 포인터 임을
보장합니다. 그러면,

int t::*ip;

는 가능합니까? 불가능하다는 것은 알고 있겠죠? 왜냐하면 t는 객체(object)이기
빼문에 범위 지정연산자(scope resolver)를 사용하는 것이 불가능합니다. 그렇다면,

int *t.ip;

는 가능합니까? 역시 불가능합니다. 위의 문장은 t객체의 ip를 참조하는
형태입니다. 그러므로 멤버변수에 관한 포인터는 항상

<member variable type> <class name>::*<id>;

라고 선언해야 합니다. SetValue()를 제대로 구현해 보면,

void CTest::SetValue(CTest& t,int i)

{

    int CTest::*ip;

    ip=&CTest::a;

    t.*ip=i;

}//CTest::SetValue

이렇게 됩니다. 완전한 소스가 아래에 리스트되어 있습니다.

#include <stdio.h>

class CTest {

    int a,b,c;

  public:

    CTest() { a=b=c=0; }

    void SetValue(CTest& t,int i);

    void Print() { printf("%d,%d,%d\n",a,b,c); }

};

void CTest::SetValue(CTest& t,int i)

{

    int CTest::*ip;

    ip=&CTest::a;

    t.*ip=i;

}//CTest::SetValue

void main()

{

    CTest t;

    t.SetValue(t,100);

    t.Print();

}//main

이제 "SetValue()가 멤버함수일 필요가 있느냐?"에 관한 의문으로되돌아 가보죠.

위의 예제에서 main()안의

t.SetValue(t.100);

은

SetValue(t,100);

으로 사용하는 것이 올바릅니다. 파라미터 t 객체의 값을 변경시키기 때분에 t. 은
쓸데없이 사용한 거죠. 그러면 SetValue()는 멤버함수가 아닌 일반 함수가 되어야
하는데요... 심각한 문제가 발생하죠. 왜냐하면 일반함수에서는 클래스의 private
멤버를 접근(access)할 수 없기 때문입니다. 그렇다고 모든 멤버를 public으로
선언할 수는 없는 문제입니다. 우리끼리 얘긴데요, 친구한테만 공개하면 되겠죠?

그러니까 SetValue()함수를 CTest 클래스의 친구로 선언하면 됩니다. friend함수는
멤버함수가 아닙니다.그러니까 정의부분이,

void CTest::SetValue(CTest& t,int i)

로 시작하면 안 됩니다. 그냥

void SetValue(CTest& t,int i)

이렇게 되어야 합니다. 완전한 소스를 적어보면 아래와 같습니다.

#include <stdio.h>

class CTest {

    int a,b,c;

  public:

    friend void SetValue(CTest& t,int i);

    CTest() { a=b=c=0; }

    void Print() { printf("%d,%d,%d\n",a,b,c); }

};

void SetValue(CTest& t,int i)

{

    int CTest::*ip;

    ip=&CTest::a;

    t.*ip=i;

}//CTest::SetValue

void main()

{

    CTest t;

    SetValue(t,100);

    t.Print();

}//main

참고: Friend function은 클래스의 private 혹은 public 영역 아무곳에나 선언 될 수
있습니다.

다음은 Static Variable에 관한 주제인데요. 이 주제는 다루기에는 너무 작아서 강의
시간에 설명한것으로 대체하겠습니다.


Pure Virtual Member Function

 Pure Virtual Member Function & Abstract Class

 한 예제 프로그램을 만들어 보면서, 왜 순수가상함수(Pure Virtual Function)가
필요한지에 대해서 알아보도록 하겠습니다. 최소한 한 개 이상의 순수가상함수를
메서드(Method)로 가진 클래스는 완전하게 정의(Define)되지 않았으므로,
실제화(Instantiation)할 수 없습니다. 이런 클래스를 추상 클래스(Abstract Class)라고
합니다.

우리가 만들 프로그램은 벌레(Bug)가 화면위를 기어다니는 에니메이션인데요, 이
벌레는 몸통(Knob)이 7개이며, 대각선의 방향으로 꾸불거리며 움직이다가, 화면의
가장자리에서 들어온 방향과 반사각의 방향으로 방향을 틀게 됩니다. 또한
깜빡거림(flickering)을 없애기 위해, Paging을 사용할 것입니다. 벌레의 모양을 대충
그려보면 아래 그림과 같습니다.

벌레는 몸톰을 꾸부려 움직이게 되는데요, 그것을 2차원상에서 에뮬레이션할려면
몸통이 처음부터 끝으로 전파되어서 마지막에 한 번 움직여야 합니다. 즉 벌레는
어느 한 순간에 위 그림처럼 굵은 몸통 한개를 가집니다. 이것을 오토마타(State
Transition Diagram, 상태 변화도)로 나타내면 1~7까지의 상태를 가집니다. 즉 1인
상태의 그림은

가 되며, 7인 상태의 그림은

가 됩니다. 그리고 1에서 7까지 변화하다가 7인 상태에서는 한칸 움직이면서
벌레의 상태를 1로 만들면 됩니다.

이제까지는 벌레의 상태를 살펴보았는데요, 벌레는 여러개의 몸통으로 이루어져
있으므로, 몸통에 대해서만 생각해 보면, 몸통은 2개의 상태를 가집니다. 즉 작은
상태는 0, 큰 상태는 1로 두면 됩니다. 그러면 어떻게 클래스를 디자인해야 할지
대충 감이 잡히죠?

CPaging, CMove, CKnob, CBug의 4개의 클래스를 디자인합니다. 이 클래스들의
계층구조(Class Hierarchy)는 아래 그림과 같습니다.

CBug는 CPaging을 상속(Inherit)받으며, CKnob는 CMove를 상속 받습니다. 또한
CBug는 CKnob의 객체 배열을 가지게(Have-a) 됩니다.

 CMove

이 클래스는 화면좌표계(Screen Coordinates)에서 움직임을 관리하는 클래스입니다.

(cx,cy) : 현재의 위치를 저장하는 멤버변수입니다.

 MoveTo(x,y) : 현재의 위치를 (x,y)로 옮기는 메서드입니다.

 StepTo(x,y) : 현재 위치에서 상대 좌표로 옮기는 메서드입니다.

 GetX(), GetY() : 각각 현재의 x,y값을 리턴합니다.

 CKnob

벌레의 몸통 클래스입니다. CMove를 상속받습니다.

state : 몸통의 상태를 유지합니다. 큰 몸통인 경우 1, 작은 몸통인 경우 0을
가집니다.

 Draw() : 현재 위치에 몸통을 현재 상태로 그립니다.

 SetState() : 몸통의 상태를 Set합니다.

 GetState() : 현재 몸통의 상태를 리턴합니다.

 CPaging

에니메이션에서 페이징을 관리하기 위한 클래스입니다.

page : 현재 작업 페이지의 번호를 가집니다.

 Paging() : 생성자는 page를 0으로 초기화합니다.(initgraph()의 디폴트 페이지는
0입니다)

 virtual Draw() : 전경(Foreground)을 그리는 루틴을 사용자가 상속받아 구현하여야
합니다.

 virtual DrawBackground() : 배경을 그리는 루틴을 사용자가 상속받아 구현하여야
합니다.

이처럼 어떤 클래스를 디자인 할 때, 반드시 있어야 하지만 그 기능은 사용자에게
맡겨진 부분이 존재할 수 있습니다. 그러므로 클래스 디자이너는 사용자에게 그
부분을 구현할 것을 알려 주어야 하는데,

virtual void UserFunc() = 0;

처럼 선언하면 됩니다. 이런 가상함수를 순수가상함수라고 합니다.
순수가상함수는 문서화(Documentation)에 도움이 되며, 클래스를 추상화시킵니다.

 CBug

실제로 벌레에 해당하는 클래스입니다. CPaging을 상속받습니다.

k[] : CKnob 클래스의 객체 배열입니다. 크기가 7이므로 7개의 몸통을 가집니다.

 state : 벌레의 상태를 유지합니다.(1~7)

 dx,dy : 벌레의 움직이는 간격을 유지합니다.

 Draw() : 상태를 보고, 현재 위치 - 몸통마다 현재 위치가 유지되고 있슴 - 에 벌레를
그립니다.

 DrawBackground() : 배경을 그립니다.

 Move() : 벌레를 한번 움직입니다.

 DrawAll() : 페이징을 하여, 배경을 그리고, 벌레를 움직이고, 벌레를 그립니다.

그러므로 main()에서는 루프안에서 DrawAll()을 연속하여 호출하면 됩니다.

 Source

#include <stdio.h>

#include <conio.h>

#include <dos.h>

#include <graphics.h>

/*-------------------constants definition-------------------*/

const int MAXKNOB=7;

const int STEP=10;

// CPaging class----------------------------------------------------------

class CPaging {

    int page;

  public:

    CPaging() { page=0; }

    void Paging() { setvisualpage(page); setactivepage(page^=1); }

    virtual void Draw() = 0;

    virtual void DrawBackground() = 0;

};//class CPaging

//CMove class---------------------------------------------------------------

class CMove {

    int cx,cy;

  public:

    void MoveTo(int x,int y) { cx=x; cy=y; }

    void StepTo(int dx,int dy) { cx+=dx; cy+=dy; }

    int GetX() { return cx; }

    int GetY() { return cy; }

};//class CMove

//CKnob class-------------------------------------------------------------

class CKnob : public CMove {

    int state;//0:small knob, 1:large knob

  public:

    void Draw();

    void SetState(int s) { state=s; }

    int GetState() { return state; }

};//class CKnob

void CKnob::Draw()

{

    int width=(GetState()==0 ? STEP-1 : STEP+1);

    setfillstyle(SOLID_FILL,WHITE);

    setcolor(WHITE);

    fillellipse(GetX(),GetY(),width,width);

}//CKnob::Draw

//CBug class--------------------------------------------------------------

class CBug : public CPaging {

    CKnob k[MAXKNOB];

    int state,

        dx,dy;

  public:

    CBug(int x=0,int y=0);

    void Draw();

    void DrawBackground();

    void Move();

    void DrawAll() { Paging(); DrawBackground(); Draw(); Move(); }

};//class CBug

CBug::CBug(int x,int y)

{

    for (int i=0;i<MAXKNOB;++i) {

        k[i].MoveTo(x,y);

        x+=STEP; y+=STEP;

    }//for

    dx=STEP; dy=STEP; state=0;

}//CBug::CBug

void CBug::Draw()

{

    for (int i=0;i<MAXKNOB;++i) {

        k[i].SetState(0);

        k[i].Draw();

    }//for

    k[state].SetState(1);

    k[state].Draw();

}//CBug::Draw

void CBug::Move()

{

    state=(state+1)%MAXKNOB;

    if (state!=0) return;

    if (k[MAXKNOB-1].GetX() <= 0 || k[MAXKNOB-1].GetX() >= 639) dx=-dx;

    if (k[MAXKNOB-1].GetY() <= 0 || k[MAXKNOB-1].GetY() >= 349) dy=-dy;

    for (int i=0;i<MAXKNOB-1;++i)//move knobs

        k[i]=k[i+1];

    k[MAXKNOB-1].StepTo(dx,dy);//set head

}//CBug::move

void CBug::DrawBackground()

{

    setfillstyle(CLOSE_DOT_FILL,GREEN);

    bar(0,0,639,349);

    setcolor(LIGHTGREEN);

}//CBug::DrawBackground

void main()

{

    int gd=VGA,gm=VGAMED;

    initgraph(&gd,&gm,"f:\\borlandc\\bgi");

    CBug b(0,0);

    b.Draw();

    while (!kbhit())

        b.DrawAll();

    closegraph();

}//main

[주] Class Diagram은 Borland VisioTM 를 사용하여 그린 것입니다.

Edited on 1997.12.16


57. 새로운 형태의 형 변환

57. 새로운 형태의 형 변
환(typecasting)

  C에서 미처 생각하지 못했던 많은 부분이 C++에 추가되었으며, 확장을 위
해 몇 개의 새로운 키워드가 C++추가되었다. 연산자에도 new, delete등이 늘
어났으며, 기능을 확장할 수 있는 메커니즘을 제공하였다. 하지만, 형 변환
(casting)에서 만큼은 C++에서도 여전히 문제가 남아 있었다. 이 문제를 해
결하기 위해서 C++ 설계팀은 형 변환을 위해 4개의 새로운 키워드를 C++에
추가하였다.

  그들은 위기에 처한 C의 형 변환을 구하기 위해 출발하였다. 그들의 이름은
const_cast, static_cast, dynamic_cast 및 reinterpret_cast였다!

   해결되지 않은 천덕꾸러기 캐스팅(casting)

  C의 형 변환에 어떤 문제점이 있는가? 독자들은 아래의 소스에서 그 문제
를 발견할 수 있는가? 주의 깊게 살펴보아야 문제점을 발견할 수 있다(필자는
이 문제점을, 책을 쓰기 시작한지 1년이 지나서야 발견할 수 있었다).

#include <iostream.h>

char i=1;

char* ip=&i;

int j=100;

const int* jp=&j;

void main() {

    int* ip2;

    int* jp2;

    ip2=(int*)ip;

    cout << *ip2 << endl;

    jp2=(int*)jp;

    cout << *jp2 << endl;

}

  실행 결과는

1

100

이다.

  문제점은 무엇인가? 아무 문제점도 보이지 않는다고? 그럴지도 모른다. 하
지만, 발견하기 어려운 문제점은 다음과 같다.

"ip2=(int*)ip; 는 문자형 포인터(char*)를 정수형 포인터(int*)로
캐스팅(casting)한 것이고, jp2=(int*)jp; 는 같은 정수형 포인터지만, jp의
const 성질을 버리기 위해서 캐스팅한 것이다. 캐스팅의 목적이 다른데,
왜 같은 형식의 (int*)x; 캐스팅을 사용해야 하는가? 이것은 분명한
문제점이다."

  위의 소스는 C++의 새로운 형 변환 연산자를 이용하여 다음과 같이 고칠
수 있다.

#include <iostream.h>

char i=1;

char* ip=&i;

int j=100;

const int* jp=&j;

void main() {

    int* ip2;

    int* jp2;

    ip2=reinterpret_cast<int*>(ip);

    cout << *ip2 << endl;

    jp2=const_cast<int*>(jp);

    cout << *jp2 << endl;

}

  출력 결과는 같다. 비록 문법적으로 까다로워 보이는 부분이 있기는 하지만,
이것은 확실히 안전한 형 변환(safe casting)을 보장한다.

   왜 캐스팅(casting)이 필요한가?

  캐스팅은 객체(object), 표현식(expression), 함수의 인자 혹은 리턴 값의 형
을 변환하기 위해 사용한다. 어떠한 변환은 컴파일러에 의해 자동으로 수행되
는데 이것을 묵시적 변환(implicit conversion)이라 한다. 표준 변환
(standard conversion)과 사용자 정의 변환(user-define conversion)⼆은 컴

             변환 연산자를 오버로딩하여 사용자 정의 형 변환을 정의할 수 있다. 또
          한 클래스 사이에는 특별한 형식의 생성자 정의를 이용하여 형 변환을 정
          의할 수 있다.

파일러에 의해 묵시적으로 행해진다. 프로그래머에 의해 명시적으로 표현된
변환을 명시적 변환(explicit conversion)이라 한다.

  표준 변환에는 다음과 같은 것이 있다.

① 정수형 승격(integral promotion)

    예) enum 을 int로 변환

② 정수형 변환(integral conversion)

    예) int를 unsigned int로 변환

③ 부동소수형 변환(floating point conversion)

    예) float를 double로 변환

④ 실수-정수 변환(float-integral conversion)

    예) int를 float로 변환

⑤ 수학적 변환(arithmetic conversion)

    예) 표현식의 평가를 위해 가장 큰 형으로 모든 피연산자를 변환

⑥ 포인터 변환(pointer conversion)

    예) 서브 클래스의 포인터를 수퍼 클래스의 포인터로 변환

⑦ 참조 변환(reference conversion)

    예) 서브 클래스의 레퍼런스를 수퍼 클래스의 레퍼런스로 변환

⑧ 멤버에 대한 포인터 변환(pointer-to-member conversion)

    예) 수퍼 클래스의 멤버에 대한 포인터를 서브 클래스의 멤버에 대한

    포인터로 변환

사용자 정의 형 변환

  우리는 이미 '연산자 오버로딩'에서 사용자 정의 형 변환을 구현하기 위한
연산자 함수(operator function)을 살펴보았다. 여기에서 언급하는 것은 연
산자 오버로딩과는 다른 것이다.

  class X를 class Y로 형 변환하기 위해서는 클래스 Y에 다음과 같은 함수
를 작성해야 한다(물론 operator X()를 정의할 수도 있다).

Y(const X& x);

  이것은 클래스 Y의 생성자처럼 보이지만, X를 Y로 형 변환해야하는 경우
불리는 함수이다. 독자들은 복사 생성자(copy constructor)를 정의하기 위해
사용했던,

X(const X& x);

를 기억하는가? 이것은 실제로 자신을 자신으로 캐스팅하는 함수를 정의한
것이다! 아래의 예를 보라.

#include <iostream.h>

class X {

    int x;

public:

    X(){ x=1; }

    void Print() {

        cout << "class X " << x << endl;

    }

};//class X

class Y {

    int y;

public:

    Y() { y=2; }

    Y(const X& x) {//class X를 class Y로 변환해야 하는 경우, 이 함수가

                    //호출된다.

        cout << "conversion from X to Y" << endl;

    }

    void Print() {

        cout << "class Y " << y << endl;

    }

};//class Y

void main() {

    X x;

    Y y;

    y.Print();

    y=x;//Y(const X& x);를 호출한 뒤, =연산자를 실행(bitwise copy)한다.

    y.Print();

}

  실행 결과는

    class Y 2

    conversion from X to Y

    class Y 1

이다. 놀랍게도 두 번째 y.Print()의 결과는 'class Y 1'이다. 하지만, 이것은
당연한 것이다. 형 변환 함수 Y(const X& x)가 호출된 후, =에 대해 연산자
오버로딩 함수를 만들지 않았으므로, 기본 =연산자인 비트 단위 복사
(bitwise copy)가 이루어진 것이다.

  C 형식의 캐스팅은

(TYPE)expression

혹은 함수 형태의

TYPE(expression)

이다. 이러한 캐스팅의 문제점은 무엇인가?

① 모든 캐스팅에 대해 문법이 같다. 이것은 프로그래머나 컴파일러로 하여금
원래의 의도를 파악하는 일을 힘들게 만든다.

② 캐스팅 대상을 찾기 힘들다. 괄호를 사용하여 복잡하게 캐스팅한 표현식에
서 캐스팅 대상을 찾기 힘들다. 이것은 문서화에 도움이 되지 못한다.

③ 비적절한 캐스팅에 대해 에러를 찾기 힘들다. C++의 캐스팅은 비적절한 캐
스팅에 대해 컴파일 시간 에러 메시지를 출력한다.

  반면에 C++의 캐스팅은 다음과 같은 이점이 있다.

① 개선된 문법. 적절한 캐스팅에 대해 각각의 문법을 제공한다.

② 개선된 의미. 캐스팅의 의도는 더 이상 모호함이 발생하지 않는다.

③ 형 안전 변환(type-safe conversion). 어떤 형 변환은 실행 시간에 안전
하게 변환될 수 있다. 적절하지 않은 형 변환은 에러를 발생하거나 NULL
을 리턴한다.

  C++의 개선된 캐스팅의 문법은 일관적이다. 예를 들면, ptr을 type T로
static_cast하기 위해서는

T* t=static_cast<T>(ptr);

처럼 사용한다.

   static_cast

  static_cast는 다음과 같은 형태를 가진다.

static_cast<T>(expr)

이것은 표현식 expr을 T로 변환한다. 이러한 캐스팅은 오직 컴파일 시간의
정보에만 의존한다. 이것은 이 캐스팅의 이름이 static인 이유이다(실행시의
형 정보를 참조하는 캐스팅의 이름은 dynamic_cast이다).

  제한을 지키기만 한다면, static_cast는 베이스 클래스의 포인터를 유도 클
래스(derived class)의 포인터로 바꾸기 위해 사용할 수 있다. 또한, 수학적인
변환, int를 enum으로 바꾸는데, 레퍼런스의 변환에, 객체의 형 변환에, 멤버
에 대한 포인터의 변환에 사용할 수 있다.

  내부적으로 컴파일러에 의해, static_cast는 표준 변환이나 사용자 정의 형
변환 같은 곳에 사용된다.

downcast

  변환이 모호하지(ambiguous) 않고, 가상 베이스 클래스(virtual base class)
가 아니라면, 베이스 클래스 포인터 X*를 유도 클래스 포인터 Y*로 캐스팅하
는 것이 가능하다. 아래의 클래스 계층을 고려해 보자.

             클래스 계층 구조: D 형을 A 형으로 변환하는 것은 모호하다. 이러한 변
          환은 C에서도 C++에서도 허용하지 않는다.

#include <iostream.h>

class A {

    int a;

public:

    A() { a=1; }

    void Print() { cout << "class A " << a << endl; }

};

class B : public A {

    int b;

public:

    B() { b=2; }

    void Print() { cout << "class B " << b << endl; }

};

class C : public A {

    int c;

public:

    C() { c=3; }

    void Print() { cout << "class C " << c << endl; }

};

class D : public B, public C {

    int d;

public:

    D() { d=4; }

    void Print() { cout << "class D " << d << endl; }

};

void f(B* pB) {

    D* pD;

    pD=static_cast<D*>(pB);

    //pD=pB;//Error: Contravents Subtype Principle

    //pD=(D*)pB;//It works, But unsafe!

    pD->Print();

}

void main() {

    D d;

    f(&d);

}

  실행 결과는

class D 4

이다. 위의 소스에서 함수 f()에 주목해 보자. pB를 pD로 캐스팅하는 문장이

pD=pB;

라면 이것은 서브 타입의 원리(subtype principle)를 위반하므로, 컴파일 시
간 에러이다. 그러므로 명시적인 형 변환이 반드시 필요한데, C 형식의

pD=(D*)pB;

를 사용할 수 있다. 이것은 분명 동작하지만, 이 변환이 안전(safe)하다라는
보장을 하지 못한다(아래의 예를 보라).

  그러므로 C++의 static_cast를 사용하는 것이 최선의 해결책⼆이다.

             static_cast는 컴파일 때 형 변환이 결정되므로, 베이스 클래스가 다형 클
          래스(polymorphic class)이면 안된다. 즉, 베이스 클래스에 가상 함수가 없어
          야 한다.

static_cast는 항상 안전한 형 변환(safe conversion)을 시도한다. 캐스팅이
안전하지 않다면 static_cast는 컴파일 시간 에러를 발생시킨다.

  만약 f()의 파라미터가 void f(A* pB) 처럼 정의된다면, 위의 소스는 2곳에
서 컴파일 시간 에러가 발생한다. main()에서 D*(즉, &d)를 A*로 캐스팅할
때, 2곳에 존재하는 클래스 A를 해결하지 못하는 모호함이 발생하므로 에러
이다. f()에서 역시 A*를 D*로 캐스팅하는 곳에서 모호함 때문에 에러가 발생
한다. 아래의 소스를 보자. 아래의 소스는 2곳에서 컴파일 시간 에러가 발생한
다(비주얼 C++ 6.0에서 컴파일했다. 비주얼 C++는 표준 C++를 완벽하게 지원
한다. 볼런드 C++ 4.5는 그렇지 않다는 것이 알려져 있다. 물론 볼런드 C++
5.0이상의 버전과 C++ Builder 시리즈는 완벽하게 C++의 표준을 지원한다).

...

void f(A* pB) {

    D* pD;

    pD=static_cast<D*>(pB);//에러가 발생한다.

    //pD=pB;//Error: Contravents Subtype Principle

    //pD=(D*)pB;//It works, But unsafe!

    pD->Print();

}

void main() {

    D d;

    f(&d);//에러가 발생한다.

}

  위의 소스는 static_cast를 사용했다. 만약 C 형식의 변환을 사용하면 어떻
게 될까? 아래의 소스를 보자.

...

void f(A* pB) {

    D* pD;

    //pD=static_cast<D*>(pB);

    //pD=pB;//Error: Contravents Subtype Principle

    pD=(D*)pB;//It works, But unsafe!

    pD->Print();

}

void main() {

    D d;

    f(&d);

}

  위의 소스 역시 컴파일 시간에 2곳에서 에러가 발생한다. static_cast를 사
용한 것과 차이가 없다(적어도 위의 소스에서는). 필자는 이 문제를 두고 많
이 고민하다가(볼런드 C++ 5.0의 도움말 및 비주얼 C++ 6.0의 MSDN을 참고
했지만, 답을 찾을 수 없었다. 물론 97년 11월에 완성된 C++ Standard Draft
에서도 적절한 예를 찾을 수 없었다) 결국 뒤로 미루었다. 이 장(chapter)이
마지막 장은 아니지만, 필자는 마지막으로 이 장을 쓰고 있다.

  97년에는 없던 많은 C++ 홈페이지(home page)가 인터넷에 생겨났다(지금
은 99년 4월 2일이다). 필자는 원하던 바를 인터넷에서 찾을 수 있었다! 진리
는 우리를 자유케 한다. 아래의 예를 보자.

#include <iostream.h>

class CBase {

private:

    int data;

public:

    CBase() : data(100) {}

    int Data() { cout << data << endl; return data; }

};//class CBase

class CDerived : private CBase {//CBase의 public 멤버는 CDerived에서

                                 //private가 된다.

public:

    CDerived() {}

};//class CDerived

void main() {

    CDerived* d=new CDerived;

    //d->Data();//private 멤버를 접근하려 했으므로, 에러이다.

    CBase* b=(CBase*)d;

    b->Data();//works!

}//main()

  실행 결과는

100

이다. 위의 소스는 실제로 컴파일되지 않아야 하는 것이다. private로 상속받
았으므로, CDerived* d를 통해 CBase 의 Data()를 접근할 수 없다. 하지만, 1
번의 형 변환으로 CBase* b를 통해 b->Data()처럼 Data를 접근한 것이다.

  이제 static_cast를 사용해 보자.

...

void main() {

    CDerived* d=new CDerived;

    CBase* b=static_cast<CBase*>(d);

    b->Data();//doesn't work!

}//main()

  위의 소스는 컴파일 시간 에러가 발생한다! C++의 새로운 형 변환은 항상
안전함(safety)를 보장한다!(몇 예를 더 찾아 볼 수 있다. 연습문제를 참고하
라)

   const_cast

  const_cast는

const_cast<T>(expr)

과 같은 형태의 문법 구조를 지닌다. 이것은 표현식 expr을 형 T로 변환한다.

  const_cast는 cv-한정자(cv-qualifier)로 선언된 변수의 const 및 volitile 성
질을 버리기 위해 사용한다.

#include <iostream.h>

int i=100;

const int* ip=&i;

void main() {

    int* ip2=const_cast<int*>(ip);

    *ip2=200;

    cout << *ip << endl;

}

  실행 결과는

200

이다. 위의 소스에서 int* ip2=const_cast<int*>(ip) 를

int* ip2=(int*)ip

로 바꾸어도 좋다. 하지만, const_cast를 사용하면, 이것은 확실히 const를 버
리기 위해 사용했다는 것을 의미하므로, 문서화에 도움이 된다.

   dynamic_cast

  dynamic_cast는

dynamic_cast<T>(expr)

과 같은 형태의 문법 구조를 지닌다. 이것은 표현식 expr을 형 T로 변환한다.

  dynamic_cast는 dynamic이 의미하듯이, 실행 시(run time)에 형 변환을 한
다. 이것은 dynamic_cast가 RTTI(run time type identification)의 일부인 것
을 의미한다. 그러므로, 컴파일러의 RTTI 옵션이 선택된 상태에서
dynamic_cast를 사용해야 한다.

             Visual C++에서 RTTI의 설정: Project->Settings 대화상자에서 RTTI를 가능
          (enable) 상태로 설정한다.

  static_cast에서 적용했던 대부분의 내용을 dynamic_cast에 대해 적용할 수
있다. 하지만, 이것이 클래스 사이의 형 변환일 때, 클래스는 반드시 다형성
클래스(polymorphic class)여야 한다. 그렇지 않다면, 컴파일 시간 에러가
발생한다. 또한, '실행 시'라는 오버헤드가 있으므로, 아주 조금 느려지는 것은
감수해야 한다.

  dynamic_cast는 실행 시에 형 변환을 시도한다. 이 변환이 안전하지 않다
면, dynamic_cast는 NULL을 리턴한다(참조에 대한 형 변환에서는 Bad_cast
예외를 발생한다. 그러므로, 실행시 에러를 막기 위해서는 try, catch를 사용해
야 한다. 이것은 연습문제에 제시한다.). 이것은 잠재하는 에러를 조기에 발견
하는 것을 돕는다. 아래의 예를 보자.

             불안전(unsafe): CDerived1과 CDerived2 사이의 형 변환은 안전하지 않다.
          이것은 실행시간에 판별이 가능하다. dynamic_cast는 실행 시간에 항상 안
          전한 형 변환을 시도한다. 안전하지 않다면, NULL을 리턴한다.

#include <iostream.h>

class CBase {

    virtual void Print()=0;

      //dynamic_cast는 polymorphic 클래스에 대해 동작한다.

};

class CDerived1 : public CBase {

    int a;

public:

    CDerived1() { a=1; }

    void Print() { cout << a << endl; }

};

class CDerived2 : public CBase {

    int b;

public:

    CDerived2() { b=2; }

    void Print() { cout << b << endl; }

};

void f(CBase* b) {

    CDerived1* d1=dynamic_cast<CDerived1*>(b);

    if (d1) {

        d1->Print();

    }

    CDerived2* d2=dynamic_cast<CDerived2*>(b);

    if (d2) {

        d2->Print();

    }

}

void main() {

    CDerived2 d;

    f(&d);

}

  실행 결과는

2

이다. main()에서의 객체가 CDerived2 의 객체이므로, f()에서 첫 번째
dynamic_cast는 불안전(unsafe)하므로, NULL을 리턴한다. 위의 결과는 2번째
if문이 만족되어서 실행된 것이다.

  위의 예에서 dynamic_cast를 C 형식의 캐스팅으로 바꾸면 어떻게 될까? C
형식의 캐스팅은 실행시 형 변환을 하지 않는다. 캐스팅은 항상 컴파일 시간
에 결정된다. 그러므로 이러한 변환은 안전하지 않다(unsafe). 아래의 예를 보
자.

...

void f(CBase* b) {

    //CDerived1* d1=dynamic_cast<CDerived1*>(b);

    CDerived1* d1=(CDerived1*)b;//unsafe!

    if (d1) {

        d1->Print();

    }

    //CDerived2* d2=dynamic_cast<CDerived2*>(b);

    CDerived2* d2=(CDerived2*)b;

    if (d2) {

        d2->Print();

    }

}

void main() {

    CDerived2 d;

    f(&d);

}

  실행 결과는

2

2

이다. 첫 번째 2는 안전하지 않은 형 변환의 결과인 것을 주목하라. 이것은 분
명한 논리적 에러이다. dynamic_cast는 항상 안전한 형 변환을 보장한다.

   reinterpret_cast

  reinterpret_cast는

reinterpret_cast<T>(expr)

과 같은 형태의 문법 구조를 지닌다. 이것은 표현식 expr을 형 T로 변환한다.

  이름이 의미하듯이, 이것은 2개의 서로 다른 형에 대해 완전히 새로운 재번
역(reinterpretation)을 시도한다. 예를 들면 정수(integer) 5를 포인터로 변환한
다면 얼마로 번역해야 하는가? 단순히 [5]번지를 의미해야 하는가? 그렇지 않
다. 세그멘테이션 방식의 운영체제에서는 [DS+5]번지로 변환할 수도 있다.

  이처럼 reinterpret_cast는 운영체제나 컴파일러에 의존적이거나, 안전하지
않은 형에 대해 형 변환을 한다. 아래의 예를 보자(볼런드 C++ 5.0의 도움말
에서 가져온 것이다).

#include <iostream.h>

void func(void *v) {

    // Cast from pointer type to integral type.

    int i = reinterpret_cast<int>(v);

    cout << i << endl;

}

void main() {

    // Cast from an integral type to pointer type.

    func(reinterpret_cast<void *>(5));

    // Cast from a pointer to function of one type to

    // pointer to function of another type.

    typedef void (* PFV)();

    PFV pfunc = reinterpret_cast<PFV>(func);

    pfunc();

}

  비주얼 C++ 6.0에서 실행 결과는

5

1

이다. reinterpret_cast는

'이것은 서로 관계없는 형 사이의 변환이다.'

는 것을 코드에 명시하므로, 확실히 문서화에 도움이 된다. 또한, 위의 main()
함수에서 2번째 예에서처럼, 안전하지 않은 형 변환을 한다. 이것은 void*를
파라미터로 받는 함수의 포인터를, 그렇지 않은 함수의 포인터로 변환했으므
로 분명히 안전하지 않다. 두 번째 f()의 호출 - pfunc() - 에서 왜 *v의 값이
1이 찍혔을까? 우리가 1을 전달한 적이 없다.

  이것은 알 수 없다. 실행 시 에러가 발생하지 않은 것으로 보아, pfunc() 호
출의 내부에서 v를 분명히 할당했을 것이고(할당하지 않았다면 GPF 에러가
발생해야 한다), (왜인지는 모르지만) 그 값이 1이었을 것이다.

  실습문제

1. C 형식의 형 변환으로 동작하지만, C++의 static_cast로 동작하지 않는 구
체적인 예를 들라.

2. 다음 프로그램을 해석하라. const 멤버 함수에서 멤버 변수의 값을 바꾸기
위해 다른 어떤 방식을 사용할 수 있는가?(힌트: mutable)

#include <iostream.h>

class CTest {

private:

    int data;

public:

    CTest() { data=100; }

    void f() const;

};//class CTest

void CTest::f() const {

    CTest* this2=const_cast<CTest*>(this);

    this2->data += 1;

    cout << data << endl;

}//CTest::f()

void main() {

    CTest t;

    t.f();

}

3. dynamic_cast를 참조에 적용했을 때, 이것이 안전하지 않다면 Bad_cast 예
외(exception)이 발생한다. 구체적인 예를 들고, try 블락을 사용하여 안전하게
고치라.

4.


59. RTTI(run time type information)

59. RTTI(run-time type
information)

  이 장의 내용은 C++의 고급 주제이며, 또한 많은 부분이 MFC(Microsoft
Foundation Class)의 RTTI를 설명하기 위해 쓰여졌다. 관심이 없는 독자는
이 장을 건너뛰어도 좋다.

  실행시 아직 정의되지 않은 클래스의 이름을 얻거나, 클래스를 동적으로 만
드는 것이 C++로 가능한가? 이것은 예전에 - C++의 표준이 완전히 정해지기
전 - 불가능했다. 하지만, 지금은 ANSI/ISO의 표준 C++ 문법이 완벽하게 이
를 지원하고 있다.

  하지만, 우리가 이 장에서 살펴 볼 내용은 C++의 새로운 키워드를 이용하
지 않고 이를 구현하고자 하는 것이다(wow!).

  왜 이것이 필요한가?

  사용자가 클래스의 이름을 입력하면, 클래스의 골격을 대충 만들어 주는 코
드 자동 생성기(automatic code generator)를 생각해 보자.

    Enter Class Name: MyClass㟺

  위의 예에서 사용자가 MyClass를 입력하면, 입력을 받은 프로그램은
MyClass란 클래스의 소스 코드 골격을 대충(하지만, 엄격히) 생성한다. 아마
도 이 소스 코드에는 자신을 동적으로 만들기 위해 메모리를 할당하는 루틴이
포함되어 있을 것이다.

  실행시에 메모리 할당이 실패했을 때,

"MyClass"를 만드는데 실패했습니다

란 에러 메시지를 출력하는 부분을 구현하고 싶다고 하자. 어떻게 이것을 구
현할 것인가? 이것은 실행시 클래스에 관한 정보(run-time type
information)를 얻어야만 가능하다. 즉, 클래스의 이름을 문자열로 얻거나, 클
래스의 크기 등을 정수로 얻을 수 있는 기능이 있어야 한다.

  실제로 많은 코드 자동 생성기 프로그램(예를 들면, Visual C++의
AppWizard, ClassWizard, C++Builder의 코드 자동 생성기, Visual Basic의 코
드 자동 생성기, Delphi의 코드 자동 생성기 등)이 지금부터 설명할 RTTI 기
능을 이용하여 클래스를 동적으로 생성하고, 실행시 클래스의 정보를 얻고 있
다(엄격히 말하면, Delphi, C++Builder와 Visual Basic등에서는 이 부분이 하
드 코딩(hard coding)되어 있어서, 링크시에 링크되기만 한다).

  먼저 클래스의 이름을 얻는 간단한 기능부터 구현해 보자.

  실행시에 객체의 클래스 이름 얻기

  독자들은 이미 가상 기능(가상 함수, 가상 베이스 클래스, 가상 소멸자),
함수 포인터(pointer to a function)에 대해 완벽하게 이해하고 있을 것이다.
이 부분에 대한 설명은 각각 '가상 함수', '함수 포인터'를 참고하라.

  아래의 예에서는 CObject 클래스가 이미 만들어져 있고, 사용자가 이 클래
스를 상속받아 CMyClass를 만드는 경우, CMyClass의 객체가 만들어졌을 때,
이 객체의 이름을 출력하는 예제이다.

  먼저 베이스 클래스인 CObject는 자신을 상속받은 하위 클래스의 이름을
얻기 위한 멤버 함수 GetClassName()을 가져야 한다. 또한 이것은 실행시에
바르게 하위 클래스를 참조해야하므로, 가상 함수■로 선언되어야 한다.

             가상 함수는 반드시 멤버 함수이므로, 가상 멤버 함수라 하지 않는다.

  CObject의 소스는 아래와 같다.

class CObject {

public:

    virtual char* GetClassName() const {

        return NULL;

    }

};//class CObject

  GetClassName()이 어떠한 값도 변경시키지 않는다는 것(즉 상수함수임을)
을 명확히 하기 위해, 끝에 const를 추가하였다. 또한 CObject 자신은 단지
베이스 클래스로만 사용될 것이므로, 자신의 이름은 NULL을 리턴하고 있다.

  사용자가 CObject를 베이스 클래스로 하면서, RTTI를 지원하는 자신의 클
래스 CMyClass를 만들기 위해서는 반드시 구현해야할 클래스의 멤버가 필요
하다. 그것은 ①자신의 클래스의 이름을 저장하는 정적 멤버 변수와 ②그 이
름을 리턴하는 GetClassName()을 오버라이드(override)하는 것이다.

  완성된 전체 소스는 아래와 같다.

//RTTI(run time type information) example

#include <iostream.h>

class CObject {

public:

    virtual char* GetClassName() const {

        return NULL;

    }

};//class CObject

class CMyClass : public CObject {

public:

    static char lpszClassName[];//클래스의 이름은 메모리에 한 번만 할

                                  //당하면 된다.

    virtual char* GetClassName() const {//베이스 클래스의 GetClassName을

                                        //오버라이드한다.

        return lpszClassName;//정적 멤버 변수를 리턴한다.

    }

};//class CMyClass

char CMyClass::lpszClassName[]="CMyClass";//정적 변수는 반드시 클래스의

                                          //외부에서 초기화되어야 한다.

void main() {

    CObject *p;

    p=new CMyClass;//subtype principle에 의해 하위 클래스를 가리키기 위

                   //해 상위 클래스의 포인터를 사용하는 것이 가능하다.

    cout << p->GetClassName() << endl;

      //동적으로 만들어진 객체의 이름을 구할 수 있다!

      //찐한 감동(^_^)이 느껴진다.

}//main

  자, 소스를 차근히 살펴보자.

class CMyClass : public CObject {

  먼저 CMyClass를 사용자가 만들 때 반드시 CObject를 상속받도록 만들어
야 한다. 만약 코드 자동 생성기가 이 부분을 생성한다면, 무조건 정해진 어떤
클래스를 상속받는 CMyClass를 만들 것이므로, 걱정하지 않아도 좋다(실제로
MFC의 AppWizard는 대부분의 클래스를 생성하는데 CObject를 베이스 클래
스로 사용한다).

static char lpszClassName[];

  그리고 클래스의 이름을 저장할 정적 변수가 클래스의 멤버로 추가되어야
한다. 또한 이 변수는 클래스의 외부에서 클래스의 이름과 같은 문자열
"CMyClass"로 초기화되어야 한다.

char CMyClass::lpszClassName[]="CMyClass";

  그리고 이름을 얻는 베이스 클래스의 GetClassName()을 오버라이드한다.

    virtual char* GetClassName() const {//베이스 클래스의 GetClassName을

                                        //오버라이드한다.

        return lpszClassName;//정적 멤버 변수를 리턴한다.

    }

  CMyClass의 GetClassName()은 단순히 자신의 정적 변수 lpszClassName을
리턴한다.

  자 이제 main()부분의 사용법을 살펴보자. 먼저 CMyClass를 동적으로 만들
기 위해 CObject* 변수 p를 선언한다.

void main() {

    CObject *p;

    p=new CMyClass;//subtype principle에 의해 하위 클래스를 가리키기 위

                   //해 상위 클래스의 포인터를 사용하는 것이 가능하다.

    cout << p->GetClassName() << endl;

      //동적으로 만들어진 객체의 이름을 구할 수 있다!

      //찐한 감동(^_^)이 느껴진다.

}//main

  p가 새로 만들어진 CMyClass의 객체를 가리킨다. p->GetClassName()은
가상 함수이므로, CObject::GetClassName()이 호출되지 않고,
CMyClass::GetClassName()을 호출하므로 출력 결과는

CMyClass

이다.

매크로의 사용

  이러 작업을 빈번하게 해야 한다면, 다음과 같은 2개의 매크로를 사용하여
일을 간단히 할 수 있다.

#define DECLARE_CLASSNAME(s) static char lpszClassName[]

#define IMPLEMENT_CLASSNAME(s) char s##::lpszClassName[]=(#s)

  위의 매크로 함수는 토큰을 결합하고 파라미터를 스트링으로 만들기 위해
각각 ##와 # 연산자를 사용하고 있다.

  기우(imagined fear): 잘 사용하지 않는 연산
자 ##와 #

  '연산자' 부분을 모두 독파(master reading)한 독자는 이미 ##와 # 연산자에
대해 알고 있겠지만, 여기서 다시 한번 이 연산자를 설명하도록 하자.

  #과 ##는 모두 매크로에서만 사용되는 연산자이다.

#: 스트링화 연산자

  #은 매크로 함수에 사용되어 매크로 함수의 파라미터를 스트링으로 만드는
역할을 한다. 아래의 예를 참고하라.

#include <iostream.h>

#define STRING(s) #s

void main() {

    cout << STRING(hello world) << endl;

}

  위의 예에서

STRING(hello world)

는 매크로 치환되어

"hello world"

로 번역된다. 다른 예를 보자. 아래의 예는 스트링과 같은 문자열 변수■를 선

             실제로 문자열 변수는 C++에 존재하지 않는다. 하지만, 의미가 명확하다
          면 char* 변수는 문자열 변수로 말하는 것은 큰 무리가 없다.

언하는 매크로를 사용하고 있다.

#include <iostream.h>

#define string_variable(s) const char *s=(#s)

void main() {

    string_variable(program_version);

    cout << program_version << endl;

}

  출력 결과는

program_version

이다.

##: 토큰(token) 연결 연산자

  ##은 매크로 함수에 사용되어 매크로 함수의 파라미터들을 결합하여 하나의
토큰■으로 만드는 역할을 하는 연산자이다. 아래의 예를 참고하라.

             컴파일러가 C++ 소스 코드를 기계어로 번역하는 과정에서 소스에서 분리
          하는 의미 있는 단위(unit)를 토큰(token)이라 한다. 예를 들면, void main()
          이라는 문장에서 토큰은 void, main, (와 )의 4개이다. 키워드는 모두 토큰
          이며, 심벌(symbol)과 사용자가 선언한 함수 이름이나 변수 이름은 모두 토
          큰이다.

#include <iostream.h>

#define token(i,j) i##j

#define string(i) lpsz##i

void main() {

    int i=1,j=2,ij=3;

    char lpszStr1[]="hello";

    char string(Str2)[]="world";

    cout << i << " " << j << " " << token(i,j) << endl;

    cout << lpszStr1 << endl;

    cout << string(Str1) << endl;

    cout << string(Str2) << endl;

}

  위의 예에서

token(i,j)

는

ij

로 번역된다. 또한

string(Str1)

은

lpszStr1

으로 번역된다. 만약 독자들이 헝가리 표기법(Hungarian notation)■을 빈번히

             변수의 종류를 구분하기 위해 변수를 선언할 때 의미 있는 접두어(prefix)
          를 붙여서 변수의 이름을 나타내는 방식을 헝가리식 표기법이라 한다. 이
          를 최초로 사용한 마이크로소프트의 헝가리 프로그래머를 기리기 위해서
          붙여진 마이크로소프트 내부의 용어였지만, 지금은 대부분의 프로그래머들
          과 컴파일러가 헝가리식 표기법을 사용한다.

사용해서, 자신이 사용하는 모든 스트링이 0으로 끝나는 것을 나타내기 위해
lpsz(long pointer to a string which is terminated by zero)를 사용한다면,
string()이라는 매크로가 이 작업을 도와줄 수 있다(어떤 독자들은 이러한 매
크로를 싫어할지 모르지만).

  이제 DECLARE_CLASSNAME()과 IMPLEMENT_CLASSNAME() 매크로
를 완벽하게 이해할 수 있을 것이다. 이 두개의 매크로는 단순한 정적 변수
선언문장과 정적 변수의 초기화를 대신한다.

  매크로를 사용하는 소스를 아래에 다시 리스트하였다.

//RTTI(run time type information) example

#include <iostream.h>

#define DECLARE_CLASSNAME(s) static char lpszClassName[]

#define IMPLEMENT_CLASSNAME(s) char s##::lpszClassName[]=(#s)

class CObject {

public:

    virtual char* GetClassName() const {

        return NULL;

    }

};//class CObject

class CMyClass : public CObject {

public:

    //static char lpszClassName[];

    DECLARE_CLASSNAME(CMyClass);

    virtual char* GetClassName() const {

        return lpszClassName;

    }

};//class CMyClass

//char CMyClass::lpszClassName[]="CMyClass";

IMPLEMENT_CLASSNAME(CMyClass);

void main() {

    CObject *p;

    p=new CMyClass;

    cout << p->GetClassName() << endl;

}//main

   왜 CObject의 GetClassName()을 순수가상함수로 구현하지 않았죠?

   CObject를 상속받는 모든 클래스가 RTTI를 지원할 필요는 없다. 즉,
CObject를 상속받는 어떤 클래스는 DECLARE_CLASSNAME등의 매크로를
포함하지 않을 것이다.

  무엇이 좋아졌는가?

  여기까지 설명을 했지만, 독자들은 여전히 개선된 사항이 무엇인지 궁금해
할 것이다. 앞으로 계속 살펴보겠지만, 개선된 사항은 위의 소스에서 분명히
나타난다.

  위의 소스에서 main()의 내부를 제외한 다른 모든 부분은 코드 자동생성기
가 자동으로 생성한 코드라는 점에 유의해야 한다(비록 여기서는 우리가 직접
소스를 입력했지만). 만약 사용자가 코드 생성기에게 동적으로 클래스의 이름
을 얻는 코드를 생성하도록 코드 생성기의 옵션(option)을 설정했다고 하자.
그러면 코드 생성기는 DECLARE_CLASSNAME과
IMPLEMENT_CLASSNAME 매크로를 위에서처럼 추가할 것이다. 사용자가
이 옵션을 선택하지 않았다고 하자. 그러면 코드 자동생성기는 단순히
DECLARE_CLASSNAME과 IMPLEMENT_CLASSNAME을 코드에 포함시
키지 않을 것이다.

  예를들면 MFC 소스 코드를 자동으로 생성하는 코드 생성기인 Application
Wizard는 이러한 매크로를 최소한 5개 이상 가지고 있다. 이 장의 뒷부분에
서 이러한 매크로를 모두 - 하지만 대충 - 살펴 볼 것이다.

  동적 생성(dynamic creation)을 지원하려면?

  이제 실제로 동적 생성을 지원하는 클래스를 만들기 위해서, 코드 자동 생
성기가 해 주어야 하는 작업을 구체적으로 살펴보자.

  실제 MFC 프로그램에서는 위에서 살펴본 lpszClassName을
CRuntimeClass 구조체가 대신한다. 이 구조체는 ①클래스 이름과, ②객체의
크기에 대한 데이터 멤버를 가지며, 대상 클래스를 동적으로 생성하기 위해,
③함수 포인터와 ④CreateObject() 멤버 함수를 가진다. 또한 ⑤클래스 계층
전체를 검색하는 - 동적으로 생성된 자신의 위치를 파악하기 위해 - 멤버 함
수를 가진다. 지금 우리는 설명을 간단히 하기 위해, ⑤는 생략하고 구현하기
로 하자.

  CRuntimeClass 구조체는 아래와 같은 모양을 가질 것이다.

struct CRuntimeClass {

    char m_lpszClassName[21];■

             MFC AppWizard는 데이터 멤버의 접두어로 m_를 붙인다. 이것은 이 변수
          가 멤버(member) 변수임을 의미한다.

    int m_nObjectSize;

    CObject* (*pfnCreateObject)();

    CObject* CreateObject();

};//struct CRunTimeClass

CObject* CRuntimeClass::CreateObject() {

    return (*pfnCreateObject)();//함수 포인터를 이용하여

                                //간접적으로 함수를 호출한다.

}//CRuntimeClass::CreateObject()

  위의 구조체에서 3개의 멤버는 데이터 멤버이고, 마지막 CreateObject()만이
멤버 함수임에 주목하라. CreateObject()는 단순히 자신의 데이터 멤버인 함수
포인터를 이용하여 무엇일지 모르는 - 아마도 사용자에 의해 정해진 객체를
생성하는 - 함수를 호출하고 있다.

  자 이제 준비 작업은 끝났다. 이제 문제는, 사용자가 정의하는 클래스가
자신을 동적으로 생성할 수 있도록 어떻게 코드를 준비하는가? 하는 것이
다.

  아마도 사용자가 정의한 클래스의 이름이 CAlpha라면 CAlpha의 멤버 함수
중에

new CAlpha;

를 가지는 문장이 반드시 있어야 할 것이다. 하지만 이러한 동적 생성 문장은
클래스의 이름에 상관없이 일관된 방법으로 호출되어야 할 것이다.

  해답은 정적 데이터 멤버(static data member)를 사용하는 것이다. 즉, 클래
스의 멤버 함수로 자신을 동적으로 생성하는 CreateObject()를 추가한다. 그리
고, 정적 CRuntimeClass 객체(예를 들면 classCObject)를 데이터 멤버로 추가
한다. 마지막으로, classCObject의 멤버 pfnCreateObject를 실제 자신을 생성
하는 함수의 주소인, &CreateObject로 초기화하는 것이다.

  CreateObject()의 주소를 얻기 위해, '멤버 함수의 주소를 얻는 방법'을 사
용하지 않은 것에 주목하라. 즉, CRuntimeClass의 함수 포인터 멤버가

CObject* (CObject::*pfnCreateObject)();

처럼 선언되어서는 안된다. 이것은 CreateObject() 역시 정적 멤버 함수이기
때문에 그렇다.

  이제 CObject의 실제 소스를 살펴보자.

class CObject {

public:

    virtual CRuntimeClass* GetRuntimeClass() const { return NULL; }

      //파생 클래스에서 반드시 구현할 필요가 없으므로

      //순수가상함수가 아니다.

    static CRuntimeClass classCObject;

    virtual ~CObject(){}

protected:

    CObject(){ printf("CObject constructor\n"); }

};//class CObject

CRuntimeClass CObject::classCObject={

    "CObject",sizeof(CObject),NULL

};

  CObject는 CRuntimeClass형의 정적 멤버 classCObject를 리턴하는
GetRuntimeClass()를 가진다. 위의 경우 CObject는 단순히 베이스 클래스로만
사용될 것이므로, 단순히 NULL을 리턴한다.

virtual CRuntimeClass* GetRuntimeClass() const { return NULL; }

  CRuntimeClass형의 정적 멤버

static CRuntimeClass classCObject;

는 외부에서 자신을 동적으로 생성하기 위해 사용한다. 이 변수는 정적 변수
이므로 클래스 외부에서 적당히 초기화된다. CObject는 단순히 베이스 클래스
로만 사용될 것이므로, 세 번째 값은 NULL로 초기화하였다.

    CRuntimeClass CObject::classCObject={■

             이 문장은 단순히 구조체를 초기화하는 문장이다. CRuntimeClass 구조체
          의 첫 번째 멤버인 m_lpszClassName을 "CObject"로 초기화한다.
          m_nObjectSize는 sizeof(CObject)로, pfnCreateObject는 NULL로 초기화한다.

        "CObject",sizeof(CObject),NULL

    };

  CRuntimeClass의 사용은 CObject를 상속받은 다른 클래스를 살펴보면, 확
실해 진다. 아래의 CAlpha는 베이스 클래스로 CObject를 사용한다.

class CAlpha : public CObject {

public:

    virtual CRuntimeClass* GetRuntimeClass() const {

        return &classCAlpha;

    }

    static CRuntimeClass classCAlpha;

      //DECLARE_DYNAMIC(CAlpha)

    static CObject* CreateObject();

      //DECLARE_DYNCREATE(CAlpha)

protected:

    CAlpha() { printf("CAlpha constructor\n"); }

};//class CAlpha

CRuntimeClass CAlpha::classCAlpha = {//IMPLEMENT_DYNAMIC(CAlpha)

    "CAlpha", sizeof(CAlpha), CAlpha::CreateObject

};

  CAlpha의 GetRuntimeClass()는 정적 멤버의 주소를 리턴한다. 정적 멤버
classCAlpha는 다음과 같이 초기화된다.

    CRuntimeClass CAlpha::classCAlpha = {//IMPLEMENT_DYNAMIC(CAlpha)

        "CAlpha", sizeof(CAlpha), CAlpha::CreateObject

    };

  구조체를 초기화하는 세 번째 값에 주목하라. 세 번째 값은 CRuntimeClass
의 세 번째 멤버인 함수 포인터를 CAlpha를 동적으로 생성하는
CreateObject()의 주소로 초기화한다. CreatObject의 소스는 아래와 같다.

    CObject* CAlpha::CreateObject() {

        return new CAlpha;//자신을 동적으로 생성한다.

    }

  어떻게 동적 생성이 가능한가?

  선언되지도 않았고, 심지어 이름이 정해지지 않았을지도 모르는 객체를 동
적으로 생성하는 문장을 자동으로 생성하는 것이 어떻게 가능한가? 이것은
정적 멤버(static member)의 특성에 있다. 정적 멤버 - 정적 멤버함수, 정적
데이터 멤버 - 는 객체와는 상관없이 항상 메모리에 존재한다는 것을 상기하
자. 그러므로, 위의 예에서 CAlpha의 정적 멤버인 classCAlpha와
CreateObject()는 CAlpha의 객체가 하나도 만들어지지 않았지만, 존재한다.

  그러므로, 이미 존재하는 classCAlpha를 이용하여, 이미 존재하는
CreateObject()를 호출함으로써, 동적 생성이 가능하게 된 것이다. 여기서 주
의할 것은 우리가 필요한 포인터는 CreateObject()가 리턴하는 포인터이지
classCAlpha의 포인터가 아니라는 것이다(classCAlpha의 포인터는 항상 일정
하다).

  아래의 예■를 참고하라. 아래의 예는 CAlpha와 CBeta를 동적으로 생성하는

             Kruglinski, "Inside Visual C++ 5.0", MicrosoftPress, 1998

예를 보여주고 있다.

매크로 사용 전 소스

//RTTI(run time type information) example

#include <stdio.h>

#include <iostream.h>

#define RUNTIME_CLASS(class_name) (&class_name::class##class_name)

class CObject;

//{{struct CRuntimeClass------------------------------------------------

struct CRuntimeClass {

    char m_lpszClassName[21];

    int m_nObjectSize;

    CObject* (*pfnCreateObject)();

    CObject* CreateObject();

};//struct CRunTimeClass

CObject* CRuntimeClass::CreateObject() {

    return (*pfnCreateObject)();//함수 포인터를 이용하여

                                //간접적으로 함수를 호출한다.

}//CRuntimeClass::CreateObject()

//}}struct CRuntimeClass------------------------------------------------

//{{class CObject-------------------------------------------------------

class CObject {

    //CObject는 순수 가상함수를 포함하지 않으므로

    //추상 클래스가 아니다. 하지만, 생성자가 protected로 설정되었으므로,

    //CObject객체를 생성할 수 없다.

public:

    virtual CRuntimeClass* GetRuntimeClass() const { return NULL; }

      //파생 클래스에서 반드시 구현할 필요가 없으므로

      //순수가상함수가 아니다.

    static CRuntimeClass classCObject;

      //DECLARE_DYNAMIC(CObject)

    virtual ~CObject(){}

protected:

    CObject(){ printf("CObject constructor\n"); }

};//class CObject

CRuntimeClass CObject::classCObject={//IMPLEMENT_DYNAMIC(CObject)

    "CObject",sizeof(CObject),NULL

};

//}}class CObject-------------------------------------------------------

//{{class CAlpha--------------------------------------------------------

class CAlpha : public CObject {

public:

    virtual CRuntimeClass* GetRuntimeClass() const {

        return &classCAlpha;

    }

    static CRuntimeClass classCAlpha;

      //DECLARE_DYNAMIC(CAlpha)

    static CObject* CreateObject();

      //DECLARE_DYNCREATE(CAlpha)

protected:

    CAlpha() { printf("CAlpha constructor\n"); }

};//class CAlpha

CRuntimeClass CAlpha::classCAlpha = {//IMPLEMENT_DYNAMIC(CAlpha)

    "CAlpha", sizeof(CAlpha), CAlpha::CreateObject

};

CObject* CAlpha::CreateObject() {//IMPLEMENT_DYNCREATE(CAlpha)

    return new CAlpha;

}

//}}class CAlpha--------------------------------------------------------

//{{class CBeta---------------------------------------------------------

class CBeta : public CObject {

public:

    virtual CRuntimeClass* GetRuntimeClass() const {

        return &classCBeta;

    }

    static CRuntimeClass classCBeta;

      //DECLARE_DYNAMIC(CBeta)

    static CObject* CreateObject();

      //DECLARE_DYNCREATE(CBeta)

protected:

    CBeta() { printf("CBeta constructor\n"); }

};//class CBeta

CRuntimeClass CBeta::classCBeta = {//IMPLEMENT_DYNAMIC(CBeta)

    "CBeta", sizeof(CBeta), CBeta::CreateObject

};

CObject* CBeta::CreateObject() {//IMPLEMENT_DYNCREATE(CBeta)

    return new CBeta;

}

//}}class CBeta---------------------------------------------------------

void main() {

    //Create CAlpha

    CRuntimeClass* pRTCAlpha=RUNTIME_CLASS(CAlpha);

    CObject* pObj1;

    pObj1=pRTCAlpha->CreateObject();

      //struct RuntimeClass의 CreateObject()가 호출되지만

      //IMPLEMENT_DYNCREATE 매크로에 의해 CAlpha의 &OnCreate()가

      //대입되어 있으므로, 결국은 CAlpha의 OnCreate()가 호출되어

      //동적으로 객체를 생성하게 된다.

      //문제를 푸는 열쇠는 바로 '함수 포인터'이다.

    printf("CAlpha class=%s\n",

     pObj1->GetRuntimeClass()->m_lpszClassName);

    //Create CBeta

    CRuntimeClass* pRTCBeta=RUNTIME_CLASS(CBeta);

    CObject* pObj2;

    pObj2=pRTCBeta->CreateObject();

    printf("CBeta class=%s\n",

    pObj2->GetRuntimeClass()->m_lpszClassName);

      //일관된 방법으로 클래스를 만들고 해제하는 것에 주목하라.

    delete pObj1;

    delete pObj2;

}//main

  실행 결과는

    CObject constructor

    CAlpha constructor

    CAlpha class=CAlpha■

            물론 CAlpha는 동적으로 얻은 CAlpha 클래스의 문자열 이름이다.

    CObject constructor

    CBeta constructor

    CBeta class=CBeta

이다.

  위의 main()에서 RUNTIME_CLASS(CAlpha)를 이용하여, 동적으로
CAlpha를 만든다음,

CRuntimeClass* pRTCAlpha=RUNTIME_CLASS(CAlpha);

 CObject의 포인터 pObj1가 이를 가리키도록 하는 사실에 주목하라.

        CObject* pObj1;

        pObj1=pRTCAlpha->CreateObject();

  사용자는 이제 pObj1을 통하여 자유롭게 CAlpha의 멤버들을 접근할 수 있
다. 그리고 객체를 해제하기 위해서

delete pRTCAlpha;

를 사용해서는 안된다.

delete pObj1;

을 사용해야 한다.

  CObject와 CRuntimeClass가 미리 만들어져 있다면, 사용자가 입력한
CAlpha에 대해 이 클래스를 동적으로 생성하는 코드가 얼마나 간단해 졌는가
에 주목하라. 이러한 작업은 매크로를 사용하여 더 간단히 줄일 수 있다.

  멤버를 선언하기 위해,

DECLARE_

매크로를 사용하며, 구현하기 위해,

IMPLEMENT_

매크로를 사용하도록 하자. 그러므로 다음과 같은 매크로를 설계할 수 있다.

#define DECLARE_DYNAMIC(class_name)	static\■

             역슬래쉬(back slash) \는 연속하는 라인이 한 줄임을 나타내는 전처리 명
          령어이다.

        CRuntimeClass class##class_name;

#define IMPLEMENT_DYNAMIC(class_name) CRuntimeClass\

                    class_name::class##class_name = {\

                        (#class_name),\

                        sizeof(class_name),\

                    class_name::CreateObject };

#define DECLARE_DYNCREATE(class_name) static CObject* CreateObject();

#define IMPLEMENT_DYNCREATE(class_name) CObject*\

             class_name::CreateObject() {\

                    return new class_name; }

  동적 형 정보에 관한 매크로는 _DYNAMIC으로 끝난다. 동적 생성에 관한
매크로는 _DYNCREATE로 끝난다. 또한 이것은 실제 MFC 코드 생성기가
사용하는 매크로이다.

매크로 사용 후 소스

//RTTI(run time type information) example

#include <stdio.h>

#include <iostream.h>

//{{RTTI macros---------------------------------------------------------

#define RUNTIME_CLASS(class_name) (&class_name::class##class_name)

#define DECLARE_DYNAMIC(class_name)	static\

        CRuntimeClass class##class_name;

#define IMPLEMENT_DYNAMIC(class_name) CRuntimeClass\

                    class_name::class##class_name = {\

                        (#class_name),\

                        sizeof(class_name),\

                    class_name::CreateObject };

#define DECLARE_DYNCREATE(class_name) static CObject* CreateObject();

#define IMPLEMENT_DYNCREATE(class_name) CObject*\

             class_name::CreateObject() {\

                    return new class_name; }

//}}RTTI macros---------------------------------------------------------

class CObject;//forward declaration of CObject

//{{struct CRuntimeClass------------------------------------------------

struct CRuntimeClass {

    char m_lpszClassName[21];

    int m_nObjectSize;

    CObject* (*pfnCreateObject)();

    CObject* CreateObject();

};//struct CRunTimeClass

CObject* CRuntimeClass::CreateObject() {

    return (*pfnCreateObject)();//함수 포인터를 이용하여

                                //간접적으로 함수를 호출한다.

}//CRuntimeClass::CreateObject()

//}}struct CRuntimeClass------------------------------------------------

//{{class CObject-------------------------------------------------------

class CObject {

    //CObject는 순수 가상함수를 포함하지 않으므로

    //추상 클래스가 아니다. 하지만, 생성자가 protected로 설정되었으므로,

    //CObject객체를 생성할 수 없다.

public:

    virtual CRuntimeClass* GetRuntimeClass() const { return NULL; }

      //파생 클래스에서 반드시 구현할 필요가 없으므로

      //순수가상함수가 아니다.

    DECLARE_DYNAMIC(CObject)■

             매크로에 이미 세미콜론 ;이 포함되어 있으므로 끝에 ;은 필요없다. 물론
          찍어 주어도 무방하다(왜 무방한가?).

      //static CRuntimeClass classCObject;

    virtual ~CObject(){}

protected:

    CObject(){ printf("CObject constructor\n"); }

};//class CObject

CRuntimeClass CObject::classCObject={

    "CObject",sizeof(CObject),NULL

};

//}}class CObject-------------------------------------------------------

//{{class CAlpha--------------------------------------------------------

class CAlpha : public CObject {

public:

    virtual CRuntimeClass* GetRuntimeClass() const {

        return &classCAlpha;

    }

    DECLARE_DYNAMIC(CAlpha)

      //static CRuntimeClass classCAlpha;

    DECLARE_DYNCREATE(CAlpha)

      //static CObject* CreateObject();

protected:

    CAlpha() { printf("CAlpha constructor\n"); }

};//class CAlpha

IMPLEMENT_DYNAMIC(CAlpha)

  //CRuntimeClass CAlpha::classCAlpha = {

  //    "CAlpha", sizeof(CAlpha), CAlpha::CreateObject

  //};

IMPLEMENT_DYNCREATE(CAlpha)

  //CObject* CAlpha::CreateObject() {

  //    return new CAlpha;

  //}

//}}class CAlpha--------------------------------------------------------

//{{class CBeta---------------------------------------------------------

class CBeta : public CObject {

public:

    virtual CRuntimeClass* GetRuntimeClass() const {

        return &classCBeta;

    }

    DECLARE_DYNAMIC(CBeta)

      //static CRuntimeClass classCBeta;

    DECLARE_DYNCREATE(CBeta)

      //static CObject* CreateObject();

protected:

    CBeta() { printf("CBeta constructor\n"); }

};//class CBeta

IMPLEMENT_DYNAMIC(CBeta)

  //CRuntimeClass CBeta::classCBeta = {

  //    "CBeta", sizeof(CBeta), CBeta::CreateObject

  //};

IMPLEMENT_DYNCREATE(CBeta)

  //CObject* CBeta::CreateObject() {

  //    return new CBeta;

  //}

//}}class CBeta---------------------------------------------------------

void main() {

    //Create CAlpha

    CRuntimeClass* pRTCAlpha=RUNTIME_CLASS(CAlpha);

    CObject* pObj1;

    pObj1=pRTCAlpha->CreateObject();

      //struct RuntimeClass의 CreateObject()가 호출되지만

      //IMPLEMENT_DYNCREATE 매크로에 의해 CAlpha의 &OnCreate()가

      //대입되어 있으므로, 결국은 CAlpha의 OnCreate()가 호출되어

      //동적으로 객체를 생성하게 된다.

      //문제를 푸는 열쇠는 바로 '함수 포인터'이다.

    printf("CAlpha class=%s\n",

     pObj1->GetRuntimeClass()->m_lpszClassName);

    //Create CBeta

    CRuntimeClass* pRTCBeta=RUNTIME_CLASS(CBeta);

    CObject* pObj2;

    pObj2=pRTCBeta->CreateObject();

    printf("CBeta class=%s\n",

     pObj2->GetRuntimeClass()->m_lpszClassName);

      //일관된 방법으로 클래스를 만들고 해제하는 것에 주목하라.

    delete pObj1;//delete pRTCAlpha는 왜 (논리적) 에러인가?

    delete pObj2;

}//main

  위의 소스는 독자들의 편의를 위해 매크로의 원래 부분을 주석으로 처리했
다. 실제 매크로의 강력함을 느끼려면 이 부분을 삭제하고 보면 확실해 진다.
CRuntimeClass 및 CObject가 이미 만들어져 있다고(afx.h에) 가정하며, 주석
을 지우고 소스를 다시 적어 보자.

  와! 매크로의 위력은 얼마나 강력한가!

매크로와 CRuntimeCass 및 CObject가 "afx.h"에 미
리 만들어져 있다고 가정했을 때의 소스 코드

#include <stdio.h>

#include <iostream.h>

#include "afx.h"

//{{class CAlpha--------------------------------------------------------

class CAlpha : public CObject {

public:

    virtual CRuntimeClass* GetRuntimeClass() const {

        return &classCAlpha;

    }

    DECLARE_DYNAMIC(CAlpha)

    DECLARE_DYNCREATE(CAlpha)

protected:

    CAlpha() { printf("CAlpha constructor\n"); }

};//class CAlpha

IMPLEMENT_DYNAMIC(CAlpha)

IMPLEMENT_DYNCREATE(CAlpha)

//}}class CAlpha--------------------------------------------------------

//{{class CBeta---------------------------------------------------------

class CBeta : public CObject {

public:

    virtual CRuntimeClass* GetRuntimeClass() const {

        return &classCBeta;

    }

    DECLARE_DYNAMIC(CBeta)

    DECLARE_DYNCREATE(CBeta)

protected:

    CBeta() { printf("CBeta constructor\n"); }

};//class CBeta

IMPLEMENT_DYNAMIC(CBeta)

IMPLEMENT_DYNCREATE(CBeta)

//}}class CBeta---------------------------------------------------------

void main() {

    //Create CAlpha

    CRuntimeClass* pRTCAlpha=RUNTIME_CLASS(CAlpha);

    CObject* pObj1;

    pObj1=pRTCAlpha->CreateObject();

    printf("CAlpha class=%s\n",

     pObj1->GetRuntimeClass()->m_lpszClassName);

    //Create CBeta

    CRuntimeClass* pRTCBeta=RUNTIME_CLASS(CBeta);

    CObject* pObj2;

    pObj2=pRTCBeta->CreateObject();

    printf("CBeta class=%s\n",

     pObj2->GetRuntimeClass()->m_lpszClassName);

    delete pObj1;

    delete pObj2;

}//main

  독자들은 매크로에 관한 설명을 전혀 듣지 않고, 위의 소스를 접했을 때의
황당함에 대해 생각해 본 적이 있는가? 필자가 MFC의 소스를 처음 접했을
때 소스는 완전한 암호문이었다. 하지만, 메시지 맵(message map)에 관한 매
크로와, 동적 생성에 관한 매크로에 대해 완전히 파악하고 난 뒤의 소스는 간
결함 그 자체였다!

  직렬화(serialize)에 관한 매크로는 설명하지 않는다. 하지만, 직렬화에 관한
매크로 역시 '동적 생성'과 비슷한 방법으로 만들어졌다고 생각하면 될 것이
다. 낮은 수준(low level)의 개념이 모두 익혀졌으면, 상위 레벨(high level)로
문제를 생각하는 것은 바람직한 접근 방법이다.

  새로운 형태의 형 변환: 추가된 키워드

  실행시 형 식별을 위해 C++ 표준이 지원하는 아래 키워드에 대한 사용법은
'새로운 형태의 형 변환'을 참고하라.

    ① const_cast

    ② reinterpret_cast

    ③ static_cast

    ④ dynamic_cast

    ⑤ typeid

  실습문제

1.


61. 이름 공간(namespace)

61. 이름 공간
(namespace)

  C++의 새로운 키워드 namespace와 using은 Visual C++ 4.0부터 지원한
다. 또한 Borland C++ 4.5에서는 지원하지 않으며, Borland C++ 5.0이후부터
는 지원한다. C++ Builder 1.0이후 버전에서 또한 지원한다1).

  1) 지금 이 글을 쓰고 있는 시간은 1998년 5월 4일이다. 현재 Visual C++ 6.0 베타 버전

    이 나와 있으며, Borland C++ Builder 3.0이 발표된 시점이다. 물론 이러한 컴파일러에

    서는 최신의 C++ 표준들을 모두 지원하고 있다.

  이름 공간은 그 안에서 선언된 기호(symbol2))들을 유일한 범위에 두게

  2) 이름 영역에는 변수, 클래스, 함수 등이 올 수 있다.

하는 선언 영역(declarative region)이다. 선언된 심벌을 namespace와 연관
시킴으로써, 그렇지 않으면 발생할지도 모르는 이름 충돌(naming conflict)
을 피할 수 있다. namespace 명칭은 문맥에 따라, 명시적으로 혹은 묵시적으
로 주어질 수 있다. 아래의 예를 보자.

namespace A

{

    class String { /*...*/ };

    String s1;  // A is implicit since within the namespace

}

A::String s2;  // A is explicit since outside the namespace

namespace B

{

    class String { /*...*/ }

      // no conflict because in another namespace

}

A::String s3;

B::String s4;

using namespace B;

String s5;  // B implicit due to using directive given above

  namespace가 클래스는 아니지만, namespace의 심벌을 참조하기 위해, 범위
해결사(scope resolver)를 사용하고 있다는데 주목하라. 많은 namespace를 가
진 프로그램에서는 using을 사용하여, 복잡한 중괄호(brace; { })를 사용하지
않을 수 있다.

  왜 namespace를 사용하는가?

  namespace를 사용함으로써 얻는 이점은 여러 가지가 있을 수 있다. 독자들
은 C로 프로그램을 코딩하면서, 연관이 없는 많은 전역 변수들 때문에 고민해
본적이 없는가? 이 때 namespace는 좋은 해결책이 될 수 있다. namespace를
사용하면, 전역 변수를 관련된 기능끼리 묶어서 관리할 수 있다.

  하지만, namespace의 처음 목적은 이것 때문이 아니었다. 독자는 자신의 프
로그램을 개발하기 위해서, 어느 두 곳으로부터 독립적인 소스를 구해서 자신
의 프로그램에서 이 두 소스를 이용하려고 할지도 모른다. 별도로 만들어진
이 두 소스가 모두 스트링을 관리하기 위해서 CString이란 클래스를 사용하
고 있다고 하자. 사용된 CString의 모호함을 어떻게 해결할 것인가? 바로
namespace가 해결책이다. 한 소스에는 namespace를 A를 주고, 다른 소스에
는 namespace를 B를 준다면, A::CString과 B::CString을 구분하는 소스를 작
성할 수 있을 것이다. 아래의 예제를 참고하라3).

  3) 아래의 예제는 C++ Builder 1.0에서 편집하고 실행하였다. 이 책의 대부분의 예제는

    Borland C++ 4.5에서 편집되고 실행한 것이지만, 이 예제는 C++ Builder에서 편집하였

    다. 최소한 C++ Builder가 Visual C++ 4.2보다는 더 표준 C++을 지원하고 있는 것 같다.

    이 예제는 Visual C++에서는 실행되지 않는다. 이탤릭체 부분은 컴파일러가 자동으로

    생성한 코드부분이다. Microsoft가 표준을 받아들이지 않고, 자신의 표준만을 업계에 내

    세우는 것은 너무나 유명하다. 독자들은 Visual C++ 5.0이상에서 지원하는

    ATL(advanced template library)을 알고 있는가? 이것은 표준 STL(standard template

    library)을 거부한 Microsoft의 반응이다. 필자는 개인적으로 Microsoft의 MFC 라이브러리

    를 싫어한다. Borland의 OWL(object window library) 혹은 VCL(visual component library)

    이 훨씬 강력하며, 빠르다. 이 시점 - 1998년 5월 4일 - 에서 C++ Builder 3.0은

    C++ 프로그래머의 최선의 선택이 될 것이다.

//---------------------------------------------------------------------

#include <vcl\condefs.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <conio>//최근의 C++은 헤더 파일에 .h를 생략하는 것을 허용한다.

#include <iostream>

#pragma hdrstop

//---------------------------------------------------------------------

USERES("main.res");

//---------------------------------------------------------------------

namespace A {

    int root=1;

    int i=2;

}

namespace B {

    int j=3;

}

int main(int argc, char **argv)

{

    cout << A::root << ' ' << A::i << endl;

    cout << B::j << endl;

    getch();

    return 0;

}

//---------------------------------------------------------------------

  C++의 새로운 #include

  소스를 살펴보기 전 먼저 #include문에 주목하자. 볼런드의 C++ 컴파일 제
품■은 헤더 파일의 확장자 .H를 생략할 수 있다.

  4) C++의 표준은 저자의 개인 홈 페이지인 http://kepler.kyungpook.ac.kr을 참

    고하라. 볼런드 이외의 컴파일러는 이러한 기능을 지원하지 않는다.

#include <conio>

는

#include <conio.h>

와 동일하다.

  namespace의 변수를 사용하는 법

  위 프로그램은 3개의 전역 변수를 사용하고 있다. 하지만 이 전역 변수는 2
개의 namespace로 구분되어 있다. 변수가 namespace안에 선언되었을 때, 이
변수를 참조하기 위해서는 반드시 namespace를 명시해야 한다. main()에서는
namespace A의 i를 참조하기 위해서 A::i 처럼 사용하고 있다.

  매번 namespace를 명시하는 것은 굉장히 번거로운 일이다. 하지만, using
을 사용하면 namespace를 매번 명시하는 작업을 하지 않아도 된다.

using namespace::A;

라는 문장은, 이 후에 사용하는 모든 변수가 namespace A를 사용함을 의미한
다. 그러므로 A::i는 i로 사용할 수 있다. 수정된 아래의 예제를 참고하라.

...

int main(int argc, char **argv)

{

    using namespace::A;

    cout << root << ' ' << i << endl;

    using namespace::B;

    cout << j << endl;

    getch();

    return 0;

}

...

-----------------------------------------------------------------------

  C++의 표준을 정하는 ANSI에서 왜 using을 구조체(structure)나 클래스(class)에 적용
하지 않는지는 굉장히 큰 의문이다. 예를 들어5),

  5) 이 글자체는 천리안 공개자료실의 '이쁜이체'이다. 이 폰트를 공개해 준, '이쁜이'님께

    감사드린다.

    struct A {

        char c;

        int i,j,k;

        long l;

    } a;

같은 구조체를 초기화하기 위해,

a.c=1; a.i=2; a.j=3; a.k=4; a.l=5;

라는 문장을 사용해야 한다. 하지만, using을 사용하여

    using a {

        c=1; i=2; j=3; k=4; l=5;

    }

처럼 사용할 수 있을 것이다. 실제로 이러한 구문은 Pascal 언어에서 with로 지원하고
있는 사항이다. 이러한 구문은 매번 사용해야 하는 클래스 이름이나 구조체 이름을 생
략할 수 있으며, 문서화에도 도움이 될 것이다.

  필자가 확신하기에 C++의 다음 표준에서 이러한 사항이 추가될 것이다.

-----------------------------------------------------------------------

  namespace는 심벌의 유일한 범위를 나타내므로, namespace에 있는 변수
이름과 같은 이름의 변수가 namespace 바깥에서 선언될 수도 있다. 하지만,
이러한 경우, 애매함(ambiguity)을 해결하기 위해서 명시적인 namescope 지정
이 반드시 필요하다. 이 때 범위 해결사(::)를 사용한다. 아래의 예제를 참고
하라.

//---------------------------------------------------------------------

#include <vcl\condefs.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <conio>

#include <iostream>

#pragma hdrstop

//---------------------------------------------------------------------

USERES("main.res");

//---------------------------------------------------------------------

int i=0;

namespace A {

    int root=1;

    int i=2;

}

namespace B {

    int j=3;

}

int main(int argc, char **argv)

{

    using namespace::A;

    //using namespace A; 처럼 사용해도 된다.

    cout << root << ' ' << ::i << ' ' << A::i << endl;

    using namespace::B;

    cout << j << endl;

    getch();

    return 0;

}

//---------------------------------------------------------------------

  위 프로그램의 출력 결과는

    1,0,2

    3

이다.

  위의 예제에서 namespace B에  int i=4; 라는 문장을 포함하면 어떻
게 될까?

using의 범위

  using이 사용되면 namescope의 적용범위는 using으로 namescope를 선언한
문장 이후부터, using이 사용된 블락의 끝까지이다.

             namespace는 블락이 끝날때까지 계속된다.

  그러므로 두개의 namespace가 같은 이름의 변수 - 예를 들면 i - 를 사용
하며, using을 이용하여 두개의 namespace가 모두 활성화된 경우, 명시적으로
같은 이름의 변수를 구분하지 않으면 - 예를 들면 A::i, B::i - 컴파일 시간
에러가 발생한다.

  클래스 안에서 using의 사용

  상속받은 하위 클래스에서 상위 클래스의 멤버 함수를 오버라이드(override)
한 경우, 하위 클래스의 객체에서6) 상위 클래스의 멤버 함수를 호출하는 것

  6) 하위 클래스를 정의하는 곳에서는 물론 상위 클래스의 멤버를 접근할 수 있다.

이 가능한가? 아래 <그림 61-1>을 참고해 보자.

  그림 61-1 상위 클래스의 멤버 함수를 오버라이딩

  클래스 B는 클래스 A를 상속받아 A의 멤버 함수 func()를 오버라이딩했다.
B의 함수 func()에서 A의 함수 func()를 A::func()처럼 명시적으로 호출하지
않는다면, A의 func()는 완전히 무시된다. 클래스 B의 인스턴스 b가 만들어
졌다고 하자.

B b;

b를 이용하여 b.func()처럼 멤버 함수를 호출하면, 클래스 B의 func()가 호출
된다. A의 func()를 호출할 수는 없는가? 가능하다. 하지만, 명시적으로 클래
스 A를 명시해야 한다. 아래의 문장은 A의 func()를 호출한다.

b.A::7)func();

  7) 객체가 만들어지고 나서도 범위 해결사(::)를 쓰는 것은 조금 이상하다. 하지만, 범위

    해결사 바로 앞에는 클래스 이름인 A가 있으므로, 이 문법은 타당하다. 범위 해결사는

    항상 클래스 이름과 연관되어 사용된다.

  지금까지의 논의는 아무 문제도 없으며, 타당하다. 하지만, A의 func()와 B
의 func()가 서로 다른 파라미터를 가지는 경우를 고려해 보자. A의 func()는

A::func(char ch);

처럼 선언되었다. B의 func()는

B::func(char *str);

처럼 선언되었다. 그러므로 클래스 B에서는 클래스 A의 func()를 오버라이딩
하고 있다.

  b.func("string")는 B의 func(char *str)을 호출할 것이다. b.A::func('c')는 A
의 func(char ch)를 호출할 것이다. 하지만, 왜 A의 func()를 호출하기 위해,
b.A::func('c')처럼 사용해야 하는가? A의 func()와 B의 func()는 서로 다른
파라미터 형을 가지므로, b.func('c')처럼 사용해도 A의 func()가 호출될 것이
아닌가? 그렇지 않다. 클래스 A의 func()는 클래스 B에서 완전하게 새로 정의
- 오버라이딩 - 되었으므로, b.func('c')는 B의 func(char *str)을 호출하려 시
도할 것이고, 이것은 컴파일 시간 에러를 발생할 것이다8). 이유는 다음과

  8) C++빌더 1.0에서는 Cannot convert 'int' to 'char *' 라는 에러 메시지가 출력된다. 위

    의 메시지에서 보면 소스에서의 char를 int로 취급하고 있는데, 이것은 아마도 C++ 빌

    더의 전신(former self)인 파스칼 언어를 사용하는 델파이(Delphi)의 잔재이거나, 혹은 아

    직 완전히 C++의 표준을 지원하지 않기 때문일 것이다. C언어에서는 char를 int로 취

    급했다. 하지만, 새로 개정된 C++에서는 char를 1byte를 차지하는 독립적인 형으로 취

    급하고 있다. Visual C++ 4.2이하, Borland C++ 4.5이하, C++ Builder 3.0이하에서는 C++

    의 표준을 따르고 있다.

같다.

"독자들은 func()를 오버라이딩(overriding)해 놓고,
오버로딩(overloading)한 것처럼 사용했기 때문이다."

  using이 키워드로 정의되기 전까지 이 문제에 대한 해결책은 없었다. 하지
만, using을 사용하면, 오버라이딩 된 상위 클래스의 멤버 함수를 오버로딩 할
수 있다. 상위 클래스 A의 오버로딩하려는 함수가 func()라면,

using A::func;

라는 문장을 하위 클래스에서 함수를 정의하는 위치에 써 주면 된다. 이것은
매우 놀라운 일이다! 아래의 <그림 61-2>를 참조하라. 클래스 B는 A의
func()를 오버라이딩 했다. 또한 오버로딩 - using A::func; - 했다.

  그림 61-2 using을 사용하면, 상위 클래스의 멤버 함수를 오버로딩 할 수 있다: 이것을

    알려 주기 위해 using을 사용하고 있다.

  아래의 예제를 참고하라.

//---------------------------------------------------------------------

#include <vcl\condefs.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <conio>

#include <iostream>

#pragma hdrstop

//---------------------------------------------------------------------

USERES("main.res");

//---------------------------------------------------------------------

// An example for accessing a namespace within a class.

// This allows us to overload a function which is a base class member.

class A {

  public:

    void func(char ch) { cout << "char = " << ch << endl; }

};

class B : public A {

  public:

  //using namespace A;// ERROR. The using directive isn't allowed

    void func(char *str) { cout << "string = " << str << endl; }

    // The using declarative

    using A::func;// Overload B::func()

};

int main(int argc, char **argv)

{

    B b;

    b.func('c');  // Calls A::func()

    b.func("c");  // Calls B::func()

    getch();

    return 0;

}

//---------------------------------------------------------------------

프로그램의 출력 결과는

    char = c

    string = c

이다.

  이름 공간의 별명(alias)

  이름 공간 명칭(identifier)을 참조하는 다른 이름을 사용하는 것이 가능하
다. 별명은 길거나, 다루기 힘든 명칭을 참조할 필요가 있을 때 유용하다. 아
鳁의 예제를 참조하라.

namespace BORLAND_INTERNATIONAL {

    /* namespace-body */

    namespace NESTED_BORLAND_INTERNATIONAL {

        /* namespace-body */

    }

}

// Alias namespace

namespace BI = BORLAND_INTERNATIONAL;

// Use access qualifier to alias a nested namespace

namespace NBI = BORLAND_INTERNATIONAL::NESTED_BORLAND\

                  _INTERNATIONAL;

  무명 이름공간(anonymous namespace)

  C++의 문법은 무명 이름 공간을 정의하는 것을 허락한다. 이것을 위해,
namespace 다음에 특별한 이름을 지정하지 않으면 된다.

namespace {// Anonymous namespace

    // Declarations

}

  이름이 지정되지 않은 모든 무명 이름 공간은 같은 파일 안에서 같은 이름
공간을 공유한다. 이것은 static 키워드 없이 static 선언을 할 수 있다는 것을
의미한다9). 무명 이름 공간에 싸여진(enclosed) 명칭은 그 파일에서만 유일

  9) static은 변수의 가시 범위를 제한한다. 만약 static이 블락 안에서 선언되었다면, 그

    블락 안에서만 보이므로(visible) 다른 블락에서 같은 이름의 변수를 사용하는 것이 가

    능하다. 만약 파일에서 static 선언이 되었다면, 다른 파일에서 같은 이름을 사용하는

    것이 가능하다.

하다.

  여러 개의 소스 파일을 사용하는 프로젝트를 고려해 보자. 파일
ANON1.CPP에서 아래처럼 무명 이름 공간을 사용한다.

#include <iostream.h>

extern void func(void);

namespace {// Anonymous

    float pi = 3.14;  // Unique identifier known only in this file

}

void main() {

   float pi = 0.1;

   cout << "pi = " << pi << endl;

   func();

}

  ANON2.CPP에서는 아래처럼 무명 이름 공간을 사용한다.

#include <iostream.h>

namespace {// Anonymous namespace

   float pi = 10.0001;  // Unique identifier known only in this file

   void func(void) {

       cout << "First func() called; pi = " << pi;

   }

}

void func(void) {

    cout << "Second func() called; pi = " << pi;

}

  프로그램의 출력 결과는 아래와 같다.

pi = 0.1

func() called; pi = 10.0001

  전이적(transitive)

  namespace안에 using을 사용하여 다른 namespace를 참조하는 것이 가능하
다. 즉, namespace는 전이적(transitive)이다. 하지만 이것은 프로그램을 읽기
어렵게 만들므로 권장하지는 않는다.

  연습문제

1.


63. 예외처리(exception handling)

63. 예외처리(exception
handling)

  예외 처리(exception handling)에 대한 논의는 예외(exception)에 대한 형
식적인 정의(formal definition)를 필요로 한다. 예외란 무엇인가?

   예외(exception)란?

  예외란

'자신이 의도하는 작업(work) 이외의 추가적인 작업을 일으키는 상황'

이라고 정의■할 수 있다.

             사실 이것은 예외의 형식적인 정의가 아니다. 예외의 형식적인 정의는
          Michael R, Lyu, "Software Fault Tolerance", WILEY, 1995에서 찾아 볼 수
          있다.

  예를 들면, 현재의 화면(screen)을 프린터로 하드 카피(hard copy)하는 프로
그램에 대해서 생각해 보자. 독자들은 화면을 메모리로 저장하여 프린터로 출
력하는 소스 코드 외에

  ① 프린터의 전원 검사(power testing)

  ② 종이의 유무 검사

  ③ 인쇄 중, 종이가 부족한 경우의 검사

  ④ 화면 전체를 메모리로 할당할 수 있는지의 검사

등을 프로그램 소스에 추가해야 할 것이다. 이러한 코드의 실행을 일으키는
상황을 예외(exception)라 하고, 예외가 발생했을 때 예외를 제어하는 코드
부분을 예외 처리 코드(exception handling code)라 한다. C++의 표준은
구조적(structural)으로 예외를 처리하는 문법을 지원한다.

  결점(fault)과 오류(error)

  대부분의 C++ 프로그래머들은 폴트(fault)와 에러(error)를 같은 것으로
생각하고 있지만, 실제 이것의 의미는 다르다. 폴트는

"원하는 대로 동작하지 않는 것"

이지만, 에러는

"이상하게 동작하는 것"

이라고 할 수 있다. 예를 들면, 사용자로부터 정수 n,m(n>m)을 입력받아 n에
서 m까지의 합을 구하는 프로그램에 대해서 생각해 보자. 사용자가 어떤 n,m
을 입력하던지 n에서 m까지의 합이 구해지지 않으면 그 프로그램은 폴트
(fault)가 있는 것이다. 하지만, 특정 n,m(예를 들면 1,40000)에 대해서 합이 구
해지지 않으면 그 프로그램은 에러가 있는 것이다.

  즉, 폴트(fault)는 소프트웨어의 동작 명세(operation specification)대로
동작하지 않는 것이며, 에러는 동작 명세대로 동작하도록 만든 프로그램의 부
분적인 결함을 말하는 것이다.

  자신이 원하는 프로그램을 자신이 직접 코딩하는 대부분의 경우, 폴트
(fault)는 존재하지 않는다. 하지만, 소프트웨어 개발 의뢰자(client)와 개발자
(developer)가 달라서, 둘 간의 충분한 의견 교환이 이루어지지 않는다면 폴
트(fault)가 존재할 것이다.

  C++의 구조적 예외 처리는 소프트웨어의 에러에 대처할 수 있는 구조적인
방법을 제공한다.

   고전적인 방법, 최근의 방법

  두 수를 나누는 작업에 대해서 생각해 보자. 어떤 이유로 분모의 값이 100
인 경우는 나눗셈을 해서는 안 된다. 또한 분모의 값이 0인 경우는 무한대이
므로, 나눗셈을 해서는 안 될 것이다. 독자들은 아마도 다음과 같은 코드를 작
성할 것이다.

#include <iostream.h>

void main() {

    int i=5,j=2,k=0;

    cout << "begin" << endl;

    if (j==100 || j==0)

        cout << j << ": Error occured" << endl;

    else

        cout << i/j << endl;

    if (k==100 || k==0)

        cout << k << ": Error occured" << endl;

    else

        cout << i/k << endl;

    cout << "end" << endl;

}

결과는

    begin

    2

    0: Error occured

    end

이다. 자 위 코드의 문제점은 무엇인가? 예외 처리를 위한 코드와 그렇지 않
은 코드가 복잡하게 얽혀있다.

  프로그램 코드에서 예외가 발생할 것 같은 부분은 특정한 몇 곳이다. 즉 코
드의 일부 - 예를 들면 5% - 에서만 예외 처리를 수행할지도 모른다. 이러한
부분이 문법적으로 명시된다면 프로그램을 유지 보수하는데 많은 이득이 있을
것이다.

  '아마도 이 곳에서 에러가 발생할 것이다' 라고 문법적으로 표시할 수는 없
을까? 또한, '지정한 곳에서의 에러 처리는 이 곳에서 한다' 라고 문법적으로
표시할 수는 없을까?

  C++의 구조적 예외 처리(structured exception handling)⼆는 문법적으로

             결점에 잘 견디는 프로그래밍(fault tolerant programming) 이론에서 제시된
          3가지 방법은 Recovery block, N-version programming 과 N-self-checking
          programming이다. 이 중 try, catch구문은 제한된 형태의 Recovery block에
          해당하는 모델이다. 실제 try, catch 구문은 Stack Unwinding을 통해 제한된
          형태의 복구(recovery)를 수행한다.

이러한 기능을 제공한다. 이러한 구조적 예외 처리를 위해

try, catch, throw■

             구조적 예외 처리가 C++의 표준으로 제안되기 전의 컴파일러인 Borland
          C++ 3.1은 try 등을 지원하지 않는다. 아래의 예들은 Visual C++ 6.0에서 테
          스트한 것이다. 물론 BC++ 4.5 이상은 try등을 지원할 것이다.

의 3개의 키워드를 사용한다.

  try, catch는 반드시 블락으로 표현된다. try 블락(try block)은 에러가 일
어날 것 같은 코드 블락 즉, 예외 처리를 수행하는 코드 블락을 나타낸다.

    try {

        예외 처리를 수행하는 코드들

    }

  catch 블락(catch block)⼆은 try에서 발생한 예외를 처리하는 코드 블락을

             catch 핸들러(handler)라고도 한다.

나타낸다.

    catch (exception_type) {

        발생한 예외에 대한 처리 코드들

    }

  단, catch 블락은 반드시 try 블락의 아래에 인접(adjacent)하게 명시되어야
하며, 여러 개 명시할 수도 있다. catch는 괄호에 파라미터를 1개 받는데, 일
반적으로 발생한 에러의 종류를 구분하기 위해 사용한다.

  그리고, 실제로 예외를 발생시키기 위해 try 블락안에서 throw 문장을 사
용할 수 있다. 예를 들면

throw 20;

은 예외 상황을 발생시키며, try 블락을 즉시 탈출하며, try 블락 아래쪽에 명
시된 catch 블락 중에서 파라미터가 20과 호환되는 것을 받는 곳(catch(int...))
으로 점프(jump)한다.

             try, catch 블락: try 블락은 소스 코드에서 에러가 발생할 것 같은 부분, 즉
          예외 처리를 해야 하는 부분을 확실하게 나타낸다. catch 블락은 try 블락
          의 밑에 인접하여, 어떤 방식으로 에러 처리를 하는지 명시한다. 이것은
          프로그램의 가독성(readability)을 높이며, 유지 보수(maintenance)에 확실히
          도움이 된다.

  try와 catch를 사용하여, 수정된 소스는 아래와 같다.

#include <iostream.h>

void main() {

    int i=5,j=2,k=0;

    cout << "begin" << endl;

      //이 문장은 try 블락안에 포함되지 않으므로, 예외 처리를 하지 않는

      //다는 것을 보장한다.

    try {//try 블락안에 포함된 문장 중에서 에러가 발생할 수도 있을 것이

         //라는 보장을 한다. try 블락은 확실히 예외 처리를 해야하는 코드

            //와 그렇지 않은 코드를 구조적으로 구분한다.

        if (j==100 || j==0) throw j;

        cout << i/j << endl;

        if (k==100 || k==0) throw k;

        cout << i/k << endl;

    }

    catch(int e) {//catch 블락은 try 블락에서 발생한 에러 처리를 실제

                   //로 수행한다.

        cout << e << ": Error occured" << endl;

    }

    cout << "end" << endl;

}

결과는

    begin

    2

    0: Error occured⼆

             try 블락에서 throw 이후의 문장은 실행되지 않는다는 것을 주의하라. 한
          번 catch 블락으로 제어가 넘겨지면, catch 블락 및 이후의 문장들을 수행
          한다.

    end

이다. 작은 변화이지만, 위의 코드는 가독성을 얼마나 증가시키는가?

  독자들은 예외 처리에 관한 코드에서 이제 try 블락 외부에 신경을 쓸 필요
가 없다. 예외는 반드시 try 블락 안에서만 발생할 것이다. 또한 try에서 발생
한 예외는 단지 throw로 던지기만 하면, catch 블락이 받는다. 즉 일관되게
catch에서 처리하는 것이다. try에서는 단지 던지기만 하고, 받은 catch에서
처리하도록 코드가 분리된 것이다!

  위의 예에서

throw j;

는 예외를 발생시켜 catch 블락의 파라미터로 j를 전달하는 것을 의미한다.
catch는 먼저 j의 형을 검사한다. 그것이 int라고 판단한 catch는 블락 중
catch (int) 형의 블락이 있는지 검사한 다음, 그곳으로 점프한다(실제로 이 검
사는 컴파일 시간에 모두 수행된다. 실행 시 throw는 단지 계산된 곳으로 점
프한다). 위의 예에서는 두 번째 throw k; 가 실행된 것이다.

  만약 catch 블락 중에서 j 혹은 k와 호환되는 파라미터로 명시된 블락이 없
으면 어떻게 될까? 자 아래의 프로그램을 실행해 보자.

#include <iostream.h>

void main() {

    int i=5,j=2,k=0;

    cout << "begin" << endl;

    try {

        if (j==100 || j==0) throw j;

        cout << i/j << endl;

        if (k==100 || k==0) throw k;

        cout << i/k << endl;

    }

    catch(char e) {

        cout << e << ": Error occured" << endl;

    }

    cout << "end" << endl;

}

  결과는

    begin

    2

    [GPF(general protection fault)가 실행 시에 발생한다.]

             Visual C++에서 실행한 경우, 실행 시 에러가 발생한다. DOS에서는
          'Abnormal program termination'이라는 메시지가 출력된 후, 프로그램은 중
          단할 것이다.

이다. throw k; 가 실행되었을 때, k를 받을 catch 블락(정수형 블락)이 없으
므로, 비정상적으로 프로그램 종료를 한 것이다.

  catch 블락이 반드시 try 블락에 인접하여야 함을 명심하라. 그러므로, 아래
의 코드는 컴파일되지 않는다.

//이 코드는 컴파일되지 않는다.

#include <iostream.h>

void main() {

    int i=5,j=2,k=0;

    cout << "begin" << endl;

    try {

        if (j==100 || j==0) throw j;

        cout << i/j << endl;

        if (k==100 || k==0) throw k;

        cout << i/k << endl;

    }

    cout << "middle" << endl;

    catch(char e) {//try와 catch를 아무도 방해해서는 안 된다.

        cout << e << ": Error occured" << endl;

    }

    cout << "end" << endl;

}

  try와 catch의 동작을 아래 그림에 나타내었다. 그림에서 보듯이 try 블락은
겹쳐질(nested) 수 있다. 이러한 예들은 계속해서 살펴 볼 것이다.

             예외 처리의 과정: try 블락이 겹쳐질 수 있음을 알 수 있다.

  위의 그림에서 보듯이, try 블락에서 던져진 예외에 대해 타당한 catch 블락
을 검색한다. catch 블락이 발견되면, 스택을 복구(unwind)하고 catch 블락으
로 컨트롤을 넘긴다.

  예외가 발생한 try 블락과 같은 레벨(level)의 catch 블락에서 타당한 catch
블락 검색에 실패하면, 상위 try 블락의 catch 블락에서 같은 검사를 수행한
다.

여러 catch 블락, 그래도 안되면...

  catch 블락은 서로 다른 형의 예외에 대하여, 여러 개 연속하여 선언할 수
있다. 아래의 예를 보자.

#include <iostream.h>

void main() {

    int i=5,j=2,k=0;

    cout << "begin" << endl;

    try {

        if (j==100 || j==0)

            throw "exception: j is 100 or 0";

        cout << i/j << endl;

        if (k==100 || k==0)

            throw "exception: k is 100 or 0";

        cout << i/k << endl;

        cout << "end of try block" << endl;

    }

    catch(char e) {

        cout << "char catch" << endl;

    }

    catch(int e) {//catch(char e)에 실패한 경우

        cout << "int catch" << endl;

    }

    catch(char* e) {//catch(int e)에 실패한 경우

        cout << e << endl;

    }

    cout << "end" << endl;

}

  실행 결과는

    begin

    2

    exception: k is 100 or 0

    end

이다. 결과 중 'exception: k is 100 or 0'를 출력한 catch 블락은 3번째
catch(char* e) 블락이다.

  위의 try 블락에서 2번째 if문을 생략하면 어떻게 될까?

    ...

    try {

        if (j==100 || j==0)

            throw "exception: j is 100 or 0";

        cout << i/j << endl;

        //if (k==100 || k==0)

        //    throw "exception: k is 100 or 0";

        cout << i/k << endl;

        cout << "end of try block" << endl;

    }

    ...

  k가 0이므로 이것은 분명한 예외 상황(분모가 0이 될 수 없다)이다. 하지만,
사용자가 명시적으로 throw를 호출하지 않았으므로, 어느 catch 블락도 'divide
by zero' 예외를 잡지(catch) 못한다. 위 프로그램은 실행 시 에러를 유발한다.

  이러한 경우 '아무거나 잡는 catch'를 설치하는 것이 바람직하다. 이 경우

catch(...⼆){}

             C++의 catch에 사용된 ...으로 인해 괄호에 단독으로 (...)처럼 명시하는 것
          이 문법적으로 가능하다. 하지만, 이것을 가변인자 파라미터(variable
          argument list)의 ...과 혼돈하지 말라. 가변 인자 파라미터의 첫 번째 파라미
          터에 ...이 올 수 없다. 이 부분은 '가변 인자 리스트'를 참고하라.

라고 명시한다. 수정된 소스를 보자.

#include <iostream.h>

void main() {

    int i=5,j=2,k=0;

    cout << "begin" << endl;

    try {

        if (j==100 || j==0)

            throw "exception: j is 100 or 0";

        cout << i/j << endl;

        //if (k==100 || k==0)

        //    throw "exception: k is 100 or 0";

        cout << i/k << endl;

        cout << "end of try block" << endl;

    }

    catch(char e) {

        cout << "char catch" << endl;

    }

    catch(int e) {

        cout << "int catch" << endl;

    }

    catch(char* e) {

        cout << e << endl;

    }

    catch(...) {

        cout << "I will catch anything!" << endl;

    }

    cout << "end" << endl;

}

실행 결과는

    begin

    2

    I will catch anything!

    end

이다. 독자들이 만약 예외 처리에 대해 전혀 신경 쓰고 싶지 않다면, 단순히
catch(...) 블락만을 명시할 수 도 있다.

try안의 try

  try 블락은 겹칠 수 있다. 아래의 예를 참고하라.

#include <iostream.h>

void main() {

    int i=5,j=2,k=0;

    cout << "begin" << endl;

    try {

        if (j==100 || j==0)

            throw "exception: j is 100 or 0";

        cout << i/j << endl;

        try {

            if (k==0) throw k⼆;

             만약 아래의 catch가 이 예외를 받지 못하면, 바깥의 try의 블락과 관계된
          catch 블락을 검사할 것이다. 만약 거기서도 예외를 받지 못하면 실행 시
          에러가 발생한다.

            cout << i/k << endl;

        }

        catch(int k) {

            cout << "nested catch block" << endl;

        }

        cout << "end of try block" << endl;

    }

    catch(char e) {

        cout << "char catch" << endl;

    }

    catch(int e) {

        cout << "int catch" << endl;

    }

    catch(char* e) {

        cout << e << endl;

    }

    catch(...) {

        cout << "I will catch anything!" << endl;

    }

    cout << "end" << endl;

}

  실행 결과는

    begin

    2

    nested catch block

    end of try block

    end

이다. 위의 예에서 'end of try block'이 인쇄된 것은 당연한 것이다. 여기서
짚고 넘어가야 할 미묘한 문제가 있다. 이것은 실습문제 1을 참고하라.

   예외의 제한 및 전파(propagation)

  throw가 반드시 try 블락 안에서 사용되어야 하지만, throw가 try블락 안에
서 호출한 함수 내부에 있다면, 이것은 에러가 아니다. 또한, try 블락에서 호
출하는 함수가 어떤 이유⼆로 예외의 종류를 제한해야 한다면, 함수를 정의할

             함수에서 예외를 제한하도록 허용한 try 설계자의 의도는 무엇일까? 몇
          가지 경우를 생각해 볼 수 있다. ① try에서 호출한 함수의 디버깅을 위해
          서: 몇 가지로 예외를 제한하여 예외의 유무를 검사하거나 디버깅을 원했
          을 것이다. ② 특수한 상황을 위한 배려: 프로그램이 죽는 것이 예외 상황
          보다 좋은 어떤 상황이 있을 수 있다. ③ 문서화를 위해: 어떤 함수가 확
          실하게 int에 대해서만 예외를 일으킨다면 throw(int)의 명시는 문서화에 도
          움이 된다. 하지만 필자는 여전히 예외를 제한하는 문법을 제공하는 이유
          에 대해서 확실히 알지 못한다.

때 원형(prototype)뒤에

throw ([type_list])

를 추가할 수 있다. 예를 들면,

    int f() throw (int,char*) {

        ...

    }

는 f()함수가 오로지 int와 char* 예외만 던지는 것을 허락한다. 또한 겹쳐진
catch 블락에서 throw를 사용하면, 에러를 바깥 catch문으로 전파
(propagation)하는 것이 가능하다.

void f() throw(int){

    cout << "f()" << endl;

    throw "exception in f()";

}

  위의 함수 f()에서 f()는 오로지 정수형의 예외만 던지는 것이 허용된다. 그
러므로, throw "excepton in f()"는 (예외 처리가 되지 않고) 실행 시 에러를
유발⼆한다.

             Visual C++ 6.0에서 이것은 실행시 에러를 유발하지 않고, 바깥의
          catch(char*) 블락으로 점프했다. 필자는 여기에 대해 아는 바 없다. 사실
          필자는 함수 뒤에 사용한 throw()에 대해 의문을 가지고 있다. 이것은 전혀
          필요하지 않을 것 같다. 아마도 Visual C++의 설계자가 필자와 같은 생각
          을 가지고 있었는지도 모른다.

void g() {

    cout << "g()" << endl;

    throw 'A';

}

  위의 함수 g()에서 사용한 throw 'A'; 는 자신을 둘러 싼 try 블락이 있다
면, 문자형의 catch 블락을 실행한다. 하지만, try 블락안에서 g()가 호출된것
이 아니라면, 프로그램은 실행 시 에러를 유발한다.

void h() {

    try {

        if (k==0) throw k;

        cout << i/k << endl;

    }

    catch(int k) {

        cout << "nested catch block" << endl;

        throw;//바깥의 catch(int)로 점프한다.

    }

}

  위의 함수 f()의 catch에서 사용한 throw;는 예외를 외부로 전파한다. 그러
므로 바깥의 catch(int)가 실행될 것이다. 물론 예외의 형을 바꾸기 위해 catch
'A'; 처럼 사용할 수도 있다. 그러면, 바깥의 catch(char)가 실행될 것이다.

  아래의 전체적인 예를 보자.

#include <iostream.h>

int i=5,j=2,k=0;

void f() throw(int){//이 함수는 int형의 예외만 던질 수 있다.

    cout << "f()" << endl;

    throw "exception in f()";

}

void g() {

    cout << "g()" << endl;

    throw 'A';//g()를 둘러싼 try가 없다면, GPF가 발생한다.

}

void h() {

    try {

        if (k==0) throw k;

        cout << i/k << endl;

    }

    catch(int k) {

        cout << "nested catch block" << endl;

        throw;//예외를 바깥 catch블락으로 전파한다.

    }

}

void main() {

    cout << "begin" << endl;

    try {

        if (j==100 || j==0)

            throw "exception: j is 100 or 0";

        cout << i/j << endl;

        //f();//독자들이 이 부분의 결과는 직접 확인해 보라.

        //g();//독자들이 이 부분의 결과는 직접 확인해 보라.

        h();

        cout << "end of try block" << endl;

    }

    catch(char e) {

        cout << "char catch" << endl;

    }

    catch(int e) {

        cout << "int catch" << endl;

    }

    catch(char* e) {

        cout << e << endl;

    }

    catch(...) {

        cout << "I will catch anything!" << endl;

    }

    cout << "end" << endl;

}

  실행 결과는

    begin

    2

    nested catch block

    int catch

    end

이다. 에러가 바깥 catch 블락으로 전파되어 'int catch'가 인쇄된 것을 알 수
있다.

기교(technique)

  예외 전파를 위한 throw는 여러 함수 호출을 빠져나가기 위한 기교로 사용
할 수 있다. 아래의 예를 보라.

             catch에 사용한 throw의 기교: 여러 함수 호출을 한꺼번에 빠져 나갈 수
          있다.

#include <iostream.h>

void f() {

    try {

        cout << "f()" << endl;

        throw "I want to exit every calls";

        cout << "end of f()" << endl;

    }

    catch(...) {

        throw;

    }

}

void g() {

    try {

        //...

        f();

    }

    catch(...) {

        throw;

    }

}

void h() {

    try {

        //...

        g();

    }

    catch(...) {

        throw;

    }

}

void main() {

    try {

        h();

        cout << "end of try in main()" << endl;

    }

    catch(...) {

        cout << "You exited directly from f()" << endl;

    }

}

  결과는

    f()

    You exited directly from f()

이다.

  컴파일러의 입장: Stack Unwinding

  예외가 catch 핸들러(handler)로 전달되었을 때, 스택 되돌리기(stack
unwinding)라고 불리는 작업이 일어난다(컴파일러가 이것을 위한 코드를 생
성한다). 스택은 지역 변수(automatic variable), 리턴 주소(return address) 및
파라미터들을 가지고 있다. 예외가 발생했을 때, 필수적으로 적당한 catch 핸
들러에 맞도록 스택의 상태는 복구되어야 한다. 그리고, (스택에 있는) 영향을
받는 지역 객체는 파괴되어야 한다.

  아래의 예를 보자. 컴파일러는 언제나 피곤하다.

#include <iostream.h>

class CTest {

    int i;

public:

    CTest(int t) {

        i=t;

        cout << "constructor: " << i << endl;

    }

    ~CTest() {

        cout << "destructor: " << i << endl;

    }

};//class CTest

void main() {

    try {

        CTest i(2),j(3);

        throw "exception";

        cout << "This will be never printed" << endl;

    }

    catch(...) {

        cout << "catch exception" << endl;

    }

}

  실행 결과는

    constructor: 2

    constructor: 3

    destructor: 3

    destructor: 2

    catch exception

이다. try 블락안에 선언된 객체는 스택에 할당되어 있으므로, catch 핸들러가
실행되기 전 파괴자가 호출되는 것이 맞다. 이러한 코드는 컴파일러가 생성한
것이다.

   표준 예외 처리 클래스(exception handling
class)

  예외를 던질 때 단순히 기본형을 넘기는 것이 아니라, 예외 처리를 위해 미
리 만들어진 클래스의 임시 객체를 넘기면 보다 자세한 정보를 catch 핸들러
가 얻을 수 있다. 아래의 예를 보자.

#include <iostream.h>

#include <string.h>

class CException {

    char name[80];

    int line;

public:

    CException(char* n,int l) {

        strcpy(name,n);

        line=l;

    }

    char* GetName() {

        return name;

    }

    int GetLine() {

        return line;

    }

};//class CException

void main() {

    try {

        cout << "This will be printed" << endl;

        throw CException("main()",__LINE__);

        cout << "This will be never printed" << endl;

    }

    catch(CException& e) {

        cout << "Excepton " << e.GetName()

             << " at line " << e.GetLine() << endl;

    }

}

실행 결과는

    This will be printed

    Exception main() at line 23

이다. 생성자를 직접 호출하여 임시 객체를 만드는 법은 '생성자,소멸자'를 참
고하고, 현재 소스의 줄 번호를 얻는 __LINE__은 '전처리 명령어'를 참고하
라. 또한, catch가 참조(reference)를 파라미터로 받는 이유는 '생성자,소멸자'의
복사 생성자(copy constructor)를 참고하라.

  iostream 및 각 컴파일러 벤더는 이러한 예외 처리 클래스를 라이브러리 수
준(library level)에서 제공한다. 각자가 사용하는 컴파일러의 도움말을 참고하
여 미리 정의된 예외 처리 클래스에 어떤 것이 있는지 반드시 확인해 보라
(MFC(Microsoft Foundation Class) 라이브러리의 경우 CException 클래스를
제공한다).

  연습문제

1. 'try안의 try'에서 제시한 예 중에,

        catch(int k) {

            cout << "nested catch block" << endl;

        }

를

        catch(char k) {

            cout << "nested catch block" << endl;

        }

로 바꾸면 결과는 무엇인가?(힌트: try 블락의 수행을 설명한 그림)

2. '예외의 제한 및 전파'에서의 예에서 함수 h()를

void h() {

    try {

        if (k==0) throw k;

        cout << i/k << endl;

    }

    catch(int k) {

        cout << "nested catch block" << endl;

        throw 'A';//예외를 바깥 catch블락으로 전파한다.

    }

}

로 수정하면 결과는 어떻게 될까?

3.


64. 주의 사항

64. 잡다한 주의 사항

  Short Circuit

  if문에서 && 혹은 ||로 연결된 모든 문장이 실행될 것을 기대해서는 안 된
다. 이것은 Short Circuit으로 알려져 있다. 아래의 if문에서 두 번째 조건 검
사문 j>k는 경우에 따라서, 실행되지 않을 수 있다.

    int i=5,j=4,k=3;

    if (i>j || j>k) cout << "short circuit";

  위에서 j>k는 실행되지 않는다. i>j가 참이므로 OR(||)로 연결된 조건은 무
조건 참이기 때문이다.

  아래의 예제를 참고하라. if문에 사용된 두 개의 함수 중 첫 번째 함수 f()만
이 실행된다. 이것은 대부분의 경우 문제가 되지 않지만, 두 개의 함수 모두가
실행되기를 기대하는 프로그래머에겐 심각한 문제가 발생한다.

#include <iostream.h>

#include <stdio.h>

int f() {

    cout << "f()" << endl;

    return 1;

}

int g() {

    cout << "g()" << endl;

    return 1;

}

void main() {

    if (f() || g()) {

        cout << "main()" << endl;

    }

}

  출력결과는

    f()

    main()

이다.

  멤버 변수의 주소

  독자들이 혼동하기 쉬운, 멤버 변수의 주소와 관계된 연산은 객체 멤버 변
수의 주소와 클래스 멤버 변수의 주소■에 관한 것이다. 아래의 예제를 참고

             이 기능의 표준 용어에 어떤 것이 있는지 확인한 바 없다. 여기서 사용한
          용어는 필자가 정의한 것이다.

하라.

#include <iostream.h>

#include <stdio.h>

class CTest {

public:

    int i;

public:

    CTest(int ti) {

        i=ti;

    }

    void Show() {

        cout << i << endl;

    }

};

void main() {

    CTest t(5),u(6);

    //t=new CTest(5);

    int* ip=&t.i;

    int CTest::*ipp=&CTest::i;

    *ip=10;

    u.*ipp=20;

    t.Show();

    u.Show();

    //delete t;

}

  실습 문제

1.


64. 키워드(keyword)

토큰(token)

  토큰은 아래와 같은 것들이 있다.

    ■ 명칭(identifier): 변수이름, 함수이름, 형이름, 클래스이름, 이름공

    간이름

    ■ 키워드(keyword): void, int 등

    ■ 리터럴(literal): 10진 상수, 문자열 상수 등

    ■ 연산자(operator): +, - 등

    ■ 구두점(punctuator): ;, :, " 등

대체 토큰(alternative token)

  비록 구현에서 제외되기는 했지만, 다음과 같은 대체 토큰이 제안되었다는
것은 흥미로운 일이다.

alternative  primary

alternative  primary

alternative  primary

<%		{

and		&&

and_eq		&=

%>		}

bitor		|

or_eq		|=

<:		[

or		||

xor_eq		^=

:>		]

xor		^

not		!

%:		#

compl		~

not_eq		!=

%:%:		##

bitand		&

             대체 토큰

  위의 대체 토큰은 #같은 특수한 기호를 사용하지 않는 국가를 위한 제안이
라고 봐야한다. 이러한 대체 토큰은 볼런드나 마이크로소프트의 컴파일러에서
지원하지 않는다. 하지만, 2번째 열(column)이후의 대체 토큰은 #define으로
정의해서 사용하면 같은 효과를 볼 수 있다.

키워드(keyword)

  아래의 단언들은 C/C++에서 사용하는 키워드들이다. 언더바(underbar: _)가
붙은 키워드는 ANSI의 표준 키워드가 아니라, 컴파일러 특정적인 것이다. 그
러한 키워드는 운영체제에 의존적인 키워드거나, 코드 최적화를 위해 필요한
것이므로, 여기서는 언급하지 않았다. 각자가 사용하는 컴파일러를 참고하기
바란다. 여기서 제시하는 키워드는 1996년 ISO C++ working paper, Borland
C++ 4.5, Visual C++ 5.0의 도움말을 참고한 것이다. 표준 키워드의 수는 모
두 62개이다.

  키워드가 언급된 장(chapter)들을 적어두었으므로, 참고하기 바란다.

asm

auto

bool

break

case

catch

cdecl(vendor specific)

char

class

const

const_cast

continue

default

delete

do

double

dynamic_cast

else

enum

explicit

extern

false

float

for

friend

goto

if

inline

int

long

mutable

namespace

new

operator

pascal(vendor specific)

private

protected

public

register

reinterpret_cast

return

short

signed

sizeof

static

static_cast

struct

switch

template

this

throw

true

try

typedef

typeid

typename

union

unsigned

using

virtual

void

volatile

wchar_t

while

연산자와 구두점(operator and punctuator)

  표준 연산자와 구두점은 아래와 같다. 각각의 역할은 '연산자'를 참고하라.

{	}	[	]	#	##	(	)	;

:	...	new	delete	?	::	.	.*	+

-	*	/	%	^	&	|	~	!

=	<	>	+=	-=	*=	/=	%=	^=

&=	|=	<<	>>	>>=	<<=	==	!=	<=

>=	&&	||	++	--	,	->*	->

리터럴(literal)

  이 책에서는 사용하지 않았지만, C++표준에는 불런 리터럴(boolean literal)
이 추가되었다.

false	true

  실습문제

1.


66. Stroustrup과의 실제 인터뷰(The Real Stroustrup Interview)

  66. Stroustrup과의 실제
인터뷰(The Real Strous
trup Interview)

  98년 국내 통신망에 'Stroustrup과의 인터뷰 내용'이라며 소개된 아래의 내
용이, C++ 프로그래머들 사이에 진위의 여부와는 상관없이 널리 퍼진 적이
있었다. 아마도 독자들 중에도 아래의 내용을 알고 있는 사람도 있을 것이다.
C++의 대부로 통하는 Stroustrup이, 인터뷰의 내용처럼 냉소적(sneer)이지는
않을 거란 생각이 지배적이었음에도 불구하고, 내용의 절묘함에 누구나 한 번
은 씁쓸한 미소를 지으며, 또한 내용의 일부에는 어느 정도 공감하였을 것이
다.

  그 내용을 여기에 소개한다.

다음 이야기의 사실 여부는 접어 두시고....

재미있게 읽어 보십시요.

  1998. 1. 1., Bjarne Stroustrup는 IEEE Computer지와 인터뷰 했다. 자연스럽
게 편집자는 그가 C++을 창조한 당사자로서 7년간의 object-oriented 설계에
대한 종합적인 의견을 보여주리라 생각했다.

  인터뷰가 끝날 즈음, Interviewer는 그의 기대 이상의 것을 알게 되었고, 편
집자는 '산업계의 이익'을 위해 그 내용을 편집하기로 하였으나, 세상 만사가
그렇듯이 비밀은 없다.

  다음은 편집되지 않은 완전한 대화 내용이며, 따라서 인터뷰 계획만큼 정리
되어 있진 않다.

---------------------------------------------------------------------

Interviewer: 예, 당신이 소프트웨어 설계의 세계를 바꾼지도 수년이 지난 지금
어떻게 생각하십니까?

Stroustrup: 사실 당신이 도착하기 전 그것을 생각하고 있었죠. 기억하십니까?
모든 사람들이 'C'를 사용하고.. 문제는 그들이 아주 전문가였다는 점입니다.
대학에서도 C를 매우 훌륭히 가르쳤습니다. 졸업생들은 아주 *유능*했습니다.
이것이 문제가 되었습니다.

Interviewer: 문제요?

Stroustrup: 예. 모든 사람들이 코볼을 쓰던 시절을 기억하십니까?

Interviewer: 물론이죠.

Stroustrup: 글쎄요, 초창기에 이들은 거의 신이었죠. 높은 보수와 귀족 대우를
받았습니다.

Interviewer: 그런 시절이었죠.

Stroustrup: 그래요. 그래서 어떻게 되었습니까? IBM은 이것에 불만이었고 프
로그래머들의 교육에 수백만불을 투자하여 마침내 백여명 정도의 인원을 길렀
습니다.

Interviewer: 그게 바로 제가 그만둔 이유입니다. 보수가 1년만에 저널리스트
보다 적은 수준으로 떨어졌습니다.

Stroustrup: 그렇습니다. 'C'프로그래머에게도 마찬가지 일이 일어났죠.

Interviewer: 그렇군요, 근데 요점이 무었입니까?

Stroustrup: 글쎄요, 하루는 제 사무실에 앉아서 보다 균형을 맞게 하기 위한
작은 계획에 대해 생각했습니다. 이런 생각을 했죠. '무척 배우기 힘든 복잡한
언어가 있다면.. 그래서 아무도 감히 프로그래머가 되려고 하지 않을 만큼.. 과
연 어떨까?'

실제로 많은 아이디어를 X 윈도우(X10)에서 가져왔습니다. 이 형편없는 그래
픽 시스템은 Sun 3/60에서만 돌았습니다. 제가 원하는 모든 요소가 여기 있었
죠. 우스꽝스러울 만큼 복잡한 문법, 애매한 함수, pseudo-OO 구조. 지금도
아무도 순전한 X 윈도우 코드를 작성치 않습니다. 제정신이라면 Motif만이 유
일한 도구이죠.

Interviewer: 진심입니까..?

Stroustrup: 사실입니다. 실제로 다른 문제도 있었습니다. 유닉스가 C로 씌어졌
지요, 즉 어떤 C 프로그래머도 쉽게 시스템 프로그래머가 될 수 있단 의미지
요. 한때 메인프레임의 시스템 프로그래머가 얼마나 벌었는지 기억하십니까?

Interviewer: 물론입니다, 제가 한때 시스템 프로그램을 했었죠.

Stroustrup: 좋습니다, 따라서 유닉스와 언어를 결합하는 모든 시스템 콜들을
감춤으로써, 새로운 언어는 유닉스와의 결별하도록 해야 했습니다. 이는 DOS
만 아는 사람들도 왠만한 소득을 벌 수 있게끔 했습니다.

Interviewer: 믿을 수 없는 예기군요...

Stroustrup: 글쎄요, 이미 시간이 지났지만 지금쯤은 사람들이 스스로 C++가
시간 낭비였다는 것 깨달았을 겁니다. 제 생각보다 훨씬 뒤늦은 일이지만요...

Interviewer: 그래서 실제로 어떤 식으로 하였습니까?

Stroustrup: 사실 단지 장난이었을 뿐이었습니다, 사람들이 제 책을 진지하게
받아들이리라 생각치 않았습니다. 두뇌가 반이라도 있다면 object-oriented 프
로그래밍이 반직관적이고, 비논리적이고 비효율적이란 걸 알 수 있습니다.

Interviewer: 뭐라구요?

Stroustrup: 또 '재사용 가능 코드'를 보세요. 한번이라도 코드를 재사용하는 회
사에 대해 들어 보셨습니까?

Interviewer: 글쎄요, 아니요, 하지만...

Stroustrup: 그렇습니다. 초기에 소수 회사가 시도는 했었죠. 오레곤의 Mentor
Graphics사가 90, 91년도에 모든 코드를 C++로 재작성 하다가 크게 혼난적이
있습니다. 이에 대해 진심으로 유감스럽게 생각했었죠. 다만, 우리는 실수로부
터 배워야 한다고 생각했습니다.

Interviewer: 물론입니다. 그래서 사람들이 교훈을 얻었습니까?

Stroustrup: 천만에요. 문제는, 대부분 회사들이 중요 실수들을 감추려 든다는
겁니다. 3천만불 손실을 주주들에게 설명하는 걸 어려워하지요. 그래도 공이
아주 없는 것은 아닙니다. 결국에는 뭔가 해내었지요.

Interviewer: 그래요? 글쎄, 그렇다면, OO가 성공했다는 거네요.

Stroustrup: 글쎄요, 거의.. 실행코드가 매우 컸습니다. 128MB RAM의 HP 웍스
테이션에서 로드하는 데 5분 걸렸습니다. 실행는 더 엄청 오래 걸렸습니다. 실
제 이것이 중요한 장애물이 되리라 생각했고 1주안에 모두 이를 알아차릴 것
으로 짐작했습니다만, 아무도 신경쓰지 않더군요. Sun과 HP는 엄청난 파워의
머신을 판매하는 데 신이났죠, 단지 작은 프로그램들을 실행키 위해 엄청난
리소스를 필요로 하는.. AT&T에서 첫 C++ 컴파일러를 가지고 'Hello world'를
컴파일 하고 2.1MB라는 믿을 수 없는 크기의 실행코드가 나왔었죠.

Interviewer: 네? 글쎄요, 컴파일러는 많이 개선되었죠, 그 이후로..

Stroustrup: 그럴까요? 최신 버젼의 g++에서 한번 해보세요. 1/2 메가 이상은
될겁니다. 또한, 세계 각지의 최근의 예들도 많습니다. British Telecom이 큰
위기를 당할뻔 했으나 운좋게 벗어나서 다시 시작할 수 있었습니다. 이들은
Australian Telecom보다 운이 좋았죠. 지금은 지멘스가 공룡을 만들고 있다는
군요. 실행코드를 저장하기 위한 하드웨어가 점점 커짐에 따라 우려도 커지고
있다고 합니다. 이래도 multiple inheritance가 좋습니까?

Interviewer: 예, 하지만 C++는 기본적으로 적절한 언어이지요.

Stroustrup: 그걸 믿습니까? 한번이라도 C++ 프로젝트를 해본 적이 있습니까?
사정은 이렇습니다: 아주 소규모의 프로젝트만이 첫 시도에 성공할 만큼 함정
을 많이 만들었습니다. 연산자 overloading을 봅시다. 프로젝트가 끝날 무렵,
거의 모든 모듈에서 이걸 사용합니다. 보통, 사람들은 교육 과정에서 그랬듯
이, 그래야만 한다고 생각하기 때문이죠. 같은 연산자가 각각의 모듈에서 제각
기 다른 의미를 갖게 됩니다. 전부 모아 놓으면 백여개의 모듈이 됩니다. 이제
data hiding을 봅시다. 각 모듈들이 서로 대화하게 함으로써 문제를 만들어 내
는 회사들을 보면 웃지 않을 수 없습니다. 'synergistic'이란 말은 프로젝트 관
리자의 가슴을 후벼파기 위해 만들어 진 게 아닌가 합니다.

Interviewer: 정말 어처구니 없군요. 프로그래머의 보수를 높이기 위해 이 모든
걸 했다구요. 한심하군요.

Stroustrup: 꼭 그렇지만 않습니다. 누구나 선택이 있습니다. 이렇게 까지 문제
가 커질 줄은 몰랐습니다. 어쨌든, 저는 기본적으로 성공했습니다. C++는 이제
죽어가고 있습니다. 하지만 프로그래머들은 여전히 높은 보수를 받습니다. 특
히 이 모든 문제들을 관리하는 불쌍한 사람들은요.. 당신이 실제로 작성한 게
아니면, 방대한 C++ 소프트웨어 모듈을 관리하는 게 불가능 한 것을 알겁니
다.

Interviewer: 어떻게요?

Stroustrup: 아는지 모르겠군요, typedef 기억하세요?

Interviewer: 그럼요.

Stroustrup: 변수 'RoofRaised'가 double precision 이란걸 겨우 찾아내기 위해
얼마나 오래 헤더 화일들을 뒤져야 하는지 아시죠? 대형 프로젝트에서 모든
클래스들에 있는 implicit한 typedef들을 찾는데 얼마나 걸릴지 생각해 보세요.

Interviewer: 그래서 어떻게 해서 성공했다는 거죠?

Stroustrup: 평균적인 'C' 프로젝트의 기간이 어느 정도 걸리죠? 약 6개월입니
다. 부인과 아이들이 있는 사람이 여유있게 살만큼 충분한 기간이 아닙니다.
동일한 프로젝트를 C++로 설계하면 어떨까요? 1년 내지 2년입니다. 대단하지
요? 잘못된 결정이 이 모든 안정된 직업을 가져온 셈입니다. 또 있습니다. 오
랜 기간 대학에서 C를 가르치지 않은 결과, 이제 훌륭한 C 프로그래머가 부
족합니다. 특히 Unix 시스템 프로그래밍의 전문가가요. 오랫동안 'new'을 써온
지금, 'malloc'을 제대로 사용할 줄 아는 사람이 몇명이나 될까요? return 값을
체크하느라 신경쓰는 일도 없죠. 실제로 대부분 C++프로그래머들은 return값을
그냥 내버립니다. '-1'을 쓰는 일은 이제 추억이 되었습니다. 적어도 'throw',
'catch', 'try' 같은 걸 쫓아다니지 않고도 에러가 있다는 걸 알 수 있던 시절이
었죠.

Interviewer: 하지만 inheritance는 시간 절약을 해주지 않습니까?

Stroustrup: 그럴까요? C프로젝트 계획과 C++프로젝트 계획의 차이를 아십니
까? C++ 프로젝트의 계획 단계가 3배는 길게 걸립니다. 어떤 부분이 inherit를
해야 하고어떤 부분이 안되는지 정확히 가려내야 합니다. 그리고 나서는, 여전
히 뭔가 잘못되어있지요. C 프로그램에서 memory leaks이 있을 수 있습니까?
지금은 이걸 찾는 게 회사들의 중요 일이 되었습니다. 대부분 회사들이 포기
하고는 그냥 제품을 내놓습니다. leak이 있다는 걸 다 알면서도 단지 그걸 찾
아내는 비용을 줄이기 위해서 입니다.

Interviewer: 그걸 해주는 tool들이 있쟎아요...

Stroustrup: 그것들의 대부분도 C++로 작성되었죠.

Interviewer: 이 인터뷰가 출판되면, 당신은 아마 린치를 당할 겁니다. 안그렇습
니까?

Stroustrup: 글쎄요. 말씀 드렸듯이 C++는 이제 전성기를 지났습니다. 정상적인
회사라면 선행 시도(pilot trial)을 안해보고 C++ 프로젝트를 착수하지 않을 겁
니다. 이를 통해서 재앙으로 가는 길이라는 걸 확인할 수 있어야 합니다. 그렇
지 못하다면 그 결과는 그들의 책임입니다. 제가 Dennis Ritchie에게 C++로
Unix를 재작성토록 하려 했단 걸 아시죠?

Interviewer: 뭐라구요. 그가 뭐라고 했습니까?

Stroustrup: 다행히 그는 유머 감각이 있습니다. 그와 Brian이 제가 무슨 짓을
하고 있는지 알아냈다고 생각합니다. 그는 제가 좋아한다면, C++ 버젼의 DOS
를 작성하는 걸 돕겠다고 했습니다.

Interviewer: 흥미가 있으셨습니까?

Stroustrup: 실제로 C++로 DOS를 작성했습니다. 끝나는 대로 demo를 드리겠
습니다. 컴퓨터실의 Sparc 20상에서 실행시키고 있습니다. 4 CPU에서 엄청난
속도로 실행되고, 70메가 정도의 디스크를 차지합니다.

Interviewer: PC에서는 어떻습니까?

Stroustrup: 농담이십니까? Windows 95아시죠? 저는 Windows 95를 저의 최대
성공으로 생각합니다. 비록 제가 준비하기도 전에 시합을 끝낸 셈이지만요.

Interviewer: Unix++에 대한 아이디어는 정말 생각해볼만 합니다. 어디선가 누
군가 시도를 하겠지요.

Stroustrup: 이 인터뷰를 읽은 다음엔 포기하겠죠.

Interviewer: 죄송합니다만, 이 인터뷰를 출판할 수 있을 것 같지 않군요.

Stroustrup: 하지만 이것은 세기의 스토리입니다. 제가 동료 프로그래머들을 위
해 한 일로 인해 제가 기억되기를 바랄 뿐입니다. 오늘날 C++ 프로그래머들이
얼마나 버는지 아십니까?

Interviewer: 제가 알기로, 제일 잘 나가는 프로그래머는 시간당 칠팔십불

정도이지요.

Stroustrup: 그렇죠? 그 정도 될겁니다. 제가 C++에 집어넣은 모든 기능을 파
악하는게 보통 일이 아닙니다. 그리고 전에 말씀드렸듯이, 모든 C++프로그래
머들이 어떤 프로젝트를 하던지, C++의 그 모든 빌어먹을 요소들을 다 사용해
야 한다는 강박관념 같은 걸 느낍니다. 이건 가끔 저를 화나게 합니다, 그게
아무리 저의 처음 의도 였지만요. 결국 저는 C++언어를 좋아합니다.

Interviewer: 전엔 좋아하지 않았습니까?

Stroustrup: 싫어했었죠. 심지어 C++가 지저분하지 않습니까? 하지만 책의 인
세가 들어오기 시작하면서... 글쎄요, 아시겠지요?

Interviewer: 잠깐요. reference는 어떤가요? C 포인터보다 개선된 것

아닙니까?

Stroustrup: 음.. 그거에 대해 항상 의문이었습니다. 처음엔 개선이라고 여겼습
니다. 근데, 하루는 C++를 처음부터 써온 친구와 얘기할 기회가 있었습니다.
그는 변수들이 reference되었는지 dereference되었는지 도무지 기억할 수가 없
어서 항상 포인터를 쓴다더군요. '*'덕분에 쉽게 알 수 있다더군요.

Interviewer: 글쎄요, 보통 이때쯤이면 '대단히 고맙습니다'라고 말하게 되는데

오늘은 그렇기 힘들겠는데요.

Stroustrup: 인터뷰를 출판해 주십시오. 요새 제 양심이 많이 좋아지고 있습니
다.

Interviewer: 나중에 알려드리겠습니다만, 편집장께서 뭐라고 할지 알 수 있습
니다.

Stroustrup: 어쨌든 누가 믿겠습니까? 테입 복사한 걸 보내주실 수 있습니까?

Interviewer: 그럼요.

The End.

  이 책의 내용이 어느 정도 완성될 무렵, IEEE에서 발행하는 잡지인
"Computer"지에 재미있는 제목으로 한 기사가 소개되었다(이 기사를 구해준
후배에게 감사한다). 기사의 제목은 "The Real Stroustrup Interview" 이었는
데, 아마도 위의 가짜 인터뷰 내용이 유명하게 네티즌(netizen)들 사이에 퍼져
있었던 것 같다. 하지만 이 기사는 실제 "Computer"지와 "Stroustrup"사이의
인터뷰 내용이며, 그의 생각과 통찰력(insight)을 알 수 있다는 것만으로도 매
우 흥분된 일이다. 비록 번역문이기는 하지만 독자들도 그의 생각을 - 약간은
건방진 듯한, 하지만 그는 그의 홈페이지에서 말하듯이 "나는 C++를 디지인
했다"라고 말할 수 있는 유일한 사람이다 - 느껴 보기를 바란다. 원어가 충실
히 번역되지 못한 부분은 원어와 함께 명시하였다.

   Bjarne Stroustrup: 그는 C++ 언어를 설계했다.

  The Real Stroustrup Interview

"Computer", pp.110-114, June, 1998.

  C++의 디자인과 발전에 있어서(Addison Wesley, 1994), Bjarne Stroustrup
은 아래와 같이 논의했다. "프로그래밍 언어는 실제로 세계(world)의 극히 작
은 부분입니다. 그러므로 그것은 너무 심각하게 간주되어서는 안 됩니다. 일부
분(proportion)의 감각(역자주: '프로그래밍 언어는 전 세계에 비하면, 매우 작
은 부분이다'는 감각)을 유지하면서, 그리고 더욱 중요하게 유모어(humor) 감
각을 유지해야 합니다. 중요한 프로그래밍 언어 중에서, C++는 익살과 농담의
풍부한 자원입니다. 그것은 우연이 아닙니다."

  지난 몇 달 동안, Stroustrup과 Computer지와의 가짜(hoax) 인터뷰가 사이
버스페이스에서 회자(make round)되었다. 우리는 그 사건에 대해 유감스럽게
(regret) 생각한다. 하지만, 그것이 우리로 하여금 C++의 대부와, 표준 C++과
일반적인 소프트웨어 개발에 대한 그의 통찰력(insight)을 나눌 좋은 기회를
허락하였다. 우리는 또한 그의 지속적인 부분(proportion)과 유머(humor)에 대
한 감각을 증명할 수 있었다(그는 그 우스꽝스러운 인터뷰를 만약 그 자신이
썼다면, 더 재미있는 개작(parody)이 되었을 거라고 제안했다).

- Scott Hamilton, Computer

표준 C++(STANDARD C++)

Computer: ISO는 표준 C++을 1997년 11월 승인하였습니다. 그리고 당신은
당신의 책 "The C++ Programming Language(Addison Wesley, 1997)"의 3번
째 판을 출판하였습니다. 지난 몇 년간 C++가 어떻게 발전하였으며, ISO 표
준이 C++ 공동체(community)에 무슨 의미를 갖는지 이야기해 주시겠습니까?

Stroustrup: 마침내 완벽하고(complete), 상세하며(detailed), 견고한(stable)
C++의 정의를 내렸다는 것은 위대한 일입니다. 이것은 C++ 공동체에 직접적
이고 간접적으로 무수한 도움이 될 것입니다. 컴파일러 제공자들은, 표준 위원
회의 내용들을 따라가는 것(catching up with the standards committee)에서
이제는 구현의 질에 관한 논쟁으로 쟁점을 이동시키기 시작함에 따라서, 분명
히 우리는 더 좋은 개발 환경을 접하게 될 것입니다. 이것은 이미 진행중입니
다.

  표준을 수용하는 구현은 툴의 혜택과 큰 공통 플랫폼을 제공하는 라이브러
리 제공자의 혜택을 경험하게 할 것입니다.

  표준은 프로그래머에게 새로운 기술과 함께 더 도전적인 기회를 제공할 것
입니다. 제품의 코드에서 비실제적인 코드를 사용했던 프로그래밍 양식은 실
제적인 계획(proposition)이 되어가고 있습니다. 이처럼, 더 유연하고(flexible),
일반적이고(general), 빠른(faster), 그리고 더 유지하기 쉬운 코드를 사용할 수
있습니다.

  일반적으로, 우리는 냉정을 유지해야 하고, "개선된" 기교의 주연(orgy)에
탐닉(indulge)해서는 안 됩니다(역자주: 개선된 C++의 기교를 사용하는 것은
중요한 일이 아니라는 주장). 기적은 여전히 없습니다. 그리고 좋은 코드는 여
전히 탄탄한 디자인과 대부분 직접적으로 매치되는 코드입니다. 그러나, 바로
지금은, 어떤 기교가 특정한 사람, 조직, 혹은 프로젝트에 적당한지, 실험하고
살펴볼 시간입니다. The C++ Programming Language의 많은 부분은 이러한
기교와 그것들이 제공하는 흥정관계(trade-offs)를 기술하고 있습니다.

  이러한 진보를 가능하게 하는, 우리가 눈으로 볼 수 있는 부분은, 언어의 새
로운 큰 기능 - 템플릿(template), 예외처리(exception), 실행시 형 정보
(runtime type information), 이름공간(namespace) - 과 표준 라이브러리입니
다. 언어의 자세한 부분의 작은 개선 또한 중요합니다. 우리는 이제 여러 해
동안 사용 가능했던 유용한 것들을 대부분 가지게 되었습니다. 하지만, 호환성
과 제품의 품질이 필요로 하는 곳에 대해서는 그것들에 크게 의존할 수 없었
습니다. 이것은 우리로 하여금, 우리의 라이브러리와 응용프로그램을 설계하는
데 있어서 최적이 아닌 차선(suboptimal)을 선택하도록 하였습니다. 그러나,
곧(올 해 1998년) 모든 큰 구현이 표준 C++를 탄탄하게 지원하게 될 것입니
다.

  언어와 프로그래밍 기교의 관점에서 C++에 가해진 가장 중요한 변화는, 정
적으로 검사 가능한 형 안정성(staticaly verifiable type safety)에 대한 강조의
계속적인 증가와 템플릿의 증가된 유연성입니다. 유연한 템플릿은 우아함
(elegance)(역자주: 명시적인 형 변환이 필요없는 C++ 프로그램은 읽기 쉽다.
이것을 우아하다고 표현한 것 같다)과 효율성(efficiency)에 주목하는 형 안정
성을 강조하기 위해서 필요합니다.

새로운 기술의 실행가능성(Feasibility of new techniques)

Computer: 당신은 말하기를 "숙련된 C++ 프로그래머조차도 지난 몇 년 동
안 인지하지 못한 것은 그러한 새로운 기능이 아니라, 기본적인 새로운 프로
그래밍 기교를 가능하게 하는 기능들간의 관계의 변화이다." 라고 하였습니다.
어떻게 이것이 표준 C++에 적용 가능한지 몇 가지 예를 들어주시겠습니까?

Stroustrup: 오버로딩이나 템플릿이 함께 사용되어 우아하고 효과적인 형 안
정 컨테이너의 중요한 역할을 한다는 것을 모르고도, 오버로딩과 템플릿은 규
칙들을 각각 공부하는 것은 쉽습니다. 이러한 연결(역자주: 오버로딩과 템플
릿의 연결)은 컨테이너에서 공통적으로 많이 사용되는 근본적인 알고리즘을
자세하게 살펴볼 때에만 명확해 집니다. 그림 1a의 프로그램 조각(segment)을
고려해 봅시다. count()의 첫 번째 호출은 정수 벡터에서 7의 횟수를 헤아립니
다. count()의 두 번째 호출은 스트링 리스트에서 "seven"의 횟수를 헤아립니
다.

void f(vector<int>& vi, list<string>* ls)

{

    int n1 = count(vi.begin(),vi.end(),7);

    int n2 = count(ls.begin(),ls.end(),"seven");

    // ...

}

(a)

template<class Iter, class T>

int count(Iter first, Iter last, T value)

{

    int c = 0;

    while (first!=last) {

        if (*first == value) c++;

        ++first;

    }

    return c;

}

(b)

            그림 1. 기능들간의 관계의 차이는 근본적인 새로운 프로그래밍 기교를 가
          능하게 한다: (a) 정수 벡터의 7을 헤아리는 알고리즘과 스트링 리스트의
          "seven"을 헤아리는 알고리즘이 템플릿을 이용하여 둘 모두를 다루는 (b)
          로 구현할 수 있다.

  오버로딩을 지원하는 각각의 언어에서 이것을 구현하는 방법은 다양합니다.
템플릿이 주어지면, 둘 모두의 호출을 다루는 하나의 함수를 그림 1b처럼 만
들 수 있습니다. 이 템플릿은 둘의 호출에 대해 최적으로 확장(역자주: 실제로
템플릿 함수는 호출되는 것이 아니라, 컴파일 시간에 템플릿을 호출한 문법에
따라 각각의 경우로 만들어진다. 그리고 실제로 만들어진 이 함수가 호출된다.
이것을 확장(expansion)이라 한다)될 것입니다. 이 count()는 표준 라이브러리
의 count()와 거의 유사합니다. 그러므로 사용자가 count()를 직접 작성할 필
요는 없습니다. count()함수는 그 자체가 오버로딩에 의존하고 있습니다: 명확
히 등호 연산자 ==는 정수와 스트링에 대해 오버로딩되어 있습니다. 게다가 *
와 ++는 각각 벡터와 리스트를 위한 반복 형(iterator type)을 위하여 "역참조
(dereference)"와 "다음 요소 참조"로 오버로딩되어 있습니다. 다시말하면, *와
++는 포인터를 위해 그것들이 가지는 의미가 주어졌습니다.

  count()의 이러한 정의는 C++ 표준 라이브러리의 컨테이너와 알고리즘에서
사용된 중요 기교의 결합을 설명합니다. 이러한 기교는 유용합니다. 그리고 그
것은 오로지 기본적인 언어의 규칙에 의해서만 간단하게 기술할 수 없습니다.

template<class Iter, class Predicatte>

int count_if(Iter first, Iter last, Predicate pred)

{

    int c = 0;

    while (first!=last) {

        if (pred(*first)) c++;

        ++first;

    }

    return c;

}

(a)

bool less_than_7(int i) { return i<7; }

void f2(vector<int>& vi, list<int>& li)

{

    int n1 = count_if(vi.begin(),vi.end(),less_than_7);

    int n2 = count_if(li.begin(),li.end(),less_than_7);

    // ...

}

(b)

            그림 2. 카운트 예제의 더 진보된 버전: (a)는 predicate를 만족하는 요소의
          개수를 헤아린다. 예를 들면 (b)는 7보다 작은 요소의 수를 헤아린다.

  그림 2a에 제시된 좀 더 진보된 예를 참조해 봅시다. 값의 횟수를 헤아리는
대신에, count_if()는 predicate를 만족시키는 요소의 개수를 헤아립니다. 예를
들면, 그림 2b처럼 7보다 작은 값을 가지는 요소의 개수를 헤아릴 수 있습니
다.

  이러한 기교를 일반화하여, 어떠한 형의 어떠한 값도 비교하는 것을 다루기
위해서는 함수 객체(function object) - 그림 3a처럼 객체가 함수처럼 불러
내어질(invoke) 수 있는 클래스 - 를 선언하는 것이 필요합니다. 그림 3b처럼
생성자 비교를 원하는 값의 참조를 저장합니다. 그리고 () 연산자는 비교를
수행합니다. 바꾸어 말하면, vi에 있는 요소중에 7보다 작은 요소의 수를 헤아
리고, ls에 있는 요소중에 "seven"보다 작은 요소의 수를 헤아립니다.

template<class T> class Less_than {

    const T v;

    public:

        Less_than(const T& vv) : v(vv) {} // constructor

        bool operator() (const T& e) { return e<v; }

};

(a)

void f3(vector<int>& vi, list<string>& ls)

{

    int n1 = count_if(vi.begin(),vi.end(),

        Less_than<int>(7));

    int n2 = count_if(ls.begin(),ls.end(),

        Less_than<string>("seven"));

    // ...

}

(b)

            그림 3. 어떤 형의 어떤 값도 비교하는 것을 다루기 위해, 우리는 클래스의
          객체가 함수처럼 불러낼 수 있는 (a) 클래스를 정의한다. 생성자는 (b)에서
          처럼, 비교하고자 하는 값의 참조를 저장한다(역자주: count_if()의 세 번째
          파라미터는 Less_than 객체를 생성하는 문장이다. 이 임시 객체가 파라미
          터로 전달된다. 물론 count_if()의 내부에서는 이 객체의 오버로딩된 연산자
          ()를 사용한다).

  생성된 코드는 매우 효율적이며, 특별히 비교를 수행하고, 다음 요소를 가져
오는 등의 작업을 하는데, 함수 호출 - 혹은 비슷한 늦은 연산자 - 을 사용하
지 않는 다는 것(역자주: 자세한 사항은 '함수 객체'를 참고하라)은 언급할 만
합니다.

  의심할 여지도 없이, 이 코드는 C++ 프로그래머가 아닌 사람에게, 혹은 템
플릿과 오버로딩의 개념을 익히지 못한 C++ 프로그램머에게 조차도 이상하게
보일 것입니다. 물론 이것은 이 예제를 여기서 제시한 이유중의 일부입니다.
그러나, 진짜 의도(bottom line)는 이 기교가 당신에게 효율적이고, 형 안정적
이고, 일반적인 코드를 작성하게 한다는 것입니다. 함수형 언어(functional
language)(역자주: LIST같은 언어를 함수형 언어라 한다)에 익숙한 사람들은
이러한 기교가 그러한 언어에서 선구적으로 개척된 기교와 유사하다는 것을
알아차렸을 것입니다.

  언어의 모습은 분리되었을 때 근본적으로 지루합니다. 그리고 그것은 좋은
시스템 설계로부터 흩뜨릴 수 있습니다(오직 언어가 살아남아야 할 프로그래
밍 기교의 측면에서).

표준 라이브러리(The Standard Library)

Computer: 표준 라이브러리는 어떻습니까? 또한 그것이 C++ 공동체에 무슨
영향을 끼칠까요?

Stroustrup: 표준 라이브러리의 가장 새롭고 흥미로운 부분은 컨테이너와 알
고리즘의 일반적이고 확장 가능한 골격(framework)입니다. 그것은 종종 STL
이라고 불리는데, 근본적으로 Alex Stepanov의 작품입니다(그 당시
Hewlett-Packard에서 지금은 Silicon Graphics에 있다).

  Alex는 확고하게(uncompromisingly) 일반적이면서 효율적인 근본적인 알고
리즘을 제공하기 위해 일을 했습니다. 예를 들면, 자료 구조에서 요소를 찾는
것, 컨테이너를 정렬하는 것, 자료 구조에서 값의 횟수를 헤아리는 것 등입니
다. 그러한 알고리즘은 많은 계산에서 근본적인 것입니다.

  Alex는 많은 종류의 언어를 가지고 연구했습니다. 그리고 몇 년간 공동 연
구자들이 있었습니다. 주목할만한 이들은 David Musser(Rensselaer
Polytechnic Institute), Meng Lee(HP Labs), Andrew Koenig(AT&T Labs)
등 입니다. 제가 STL에 공헌한 부분은 매우 작지만 중요하다고 생각합니다:
저는 어댑터(adapter)라고 불리는 mem_fun()을 디자인했습니다. 그것은 다형
객체(polymorphic object)의 컨테이너를 위해서 표준 알고리즘이 사용되는 것
을 제공합니다(역자주: 어댑터는 크기가 동적인 가변 객체를 컨테이너 연산의
대상으로 하기 위한 메모리 할당 및 처리 루틴을 말하는 것 같다). 이처럼, 객
체 지향 기술을 시도하는 것이 깔끔하게 표준 라이브러리의 일반적인 프로그
래밍 골격에 포함되었습니다.

  다소 놀랍게도, STL은 제가 몇 년 전에 개발한 C++을 위한 표준 컨테이너
의 좋은 집합을 결정하는 판단기준과 일치되었습니다. 1년 후, STL에 대해
의논하고 마무리한 후, ISO C++ 위원회는 STL을 C++를 위한 표준 라이브러
리의 중요한 부분으로 받아들였습니다. 저의 판단기준도 포함되었습니다.

  ○ 간단한 기본 연산의 확고한 효율성(예를 들면, 배열의 인덱싱
(subscripting)이 함수 호출의 비용을 유발해서는 안 된다);

  ○ 형 안정성(type-safety)(컨테이너로부터의 객체는 명시적인(explicit) 혹은
함축적인(implicit) 형 변환 없이 사용 가능해야 한다);

  ○ 일반성(라이브러리는 벡터,리스트,맵 같은 몇 개의 컨테이터를 제공해야
한다);

  ○ 확장성(내가 필요로하는 컨테이너를 라이브러리가 제공하지 못하는 경
우, 내가 컨테이터를 만들어 표준 컨테이너처럼 사용할 수 있어야 한다).

  약간의 수정과 추가를 거친 STL을 채택함으로써, 위원회에서 해야 할 끔찍
한 디자인을 피할 수 있었습니다.

  명확히, C++ 표준 위원회의 시간제 근무자의 그룹이 프로그래머가 유용하
다고 판단하는 모든 라이브러리의 기능을 제공할 수 없습니다. 결과적으로 중
요한 쟁점은 무엇이 표준 라이브러리에 포함되어야 하고, 무엇이 기업과 개인
이 제공하는 것으로 남겨져야 하는 것을 결정하는 것이었습니다.

  우리는 무엇이 포함되어야 하는가의 길잡이로 "독립적으로 개발된 라이브러
리 사이의 통신 역할을 하는데 필요한 것"을 사용하기로 하였습니다. 이처럼,
I/O, 스트링, 컨테이너가 큰 촛점이 되었습니다. 우리는 표준 C 라이브러리와
수 계산을 위한 몇 기능을 역사적인 이유 때문에 포함시켰습니다. 하지만, 잠
재적으로 유용한 많은 기능들 - 날짜와 통화, 정규 표현 매칭, 그래픽 - 을
남겨두었습니다. 다행히도, 이러한 기능들은 공개 혹은 상업적으로 이용 가능
합니다.

  표준 라이브러리는 프로그래머가 바퀴를 다시 발명하는 것을 구해냅니다.
특별히, 표준 컨테이너는 초보자나 숙련자 모두에게 높은 수준의 프로그램
(high level programming)을 허락합니다. 쉬운 일을 간단히 처리하는, 그림 4
의 간단한 프로그램을 고려해 봅시다.

int main()

{

    cout << "Please enter your first name:\n";

    string name;

    cin >> name; // read into name

    cout << "Hello" << name << "!\n";

}

            그림 4. 간단하게 일을 처리하는 "Hello" 프로그램

  매크로도 이용하지 않고, 명시적인 메모리 관리나 다른 고 수준 언어의 기
능을 이용하지 않으며, 리소스의 제한도 받지 않습니다. 특별히, 만약 어떤 익
살꾼(joker)이 자신의 첫 번째 이름으로 30,000자를 적는다고 하더라도, 에러
나 오버플로우 없이 프로그램은 충실히 이름을 출력할 것입니다.

  표준 라이브러리를 이용하는 것은 C++가 배워지던 방법의 혁명을 일으킬
수 있고 일으켜야 합니다. 이제 C++을 고수준 언어로써 배우는 것이 가능합
니다. 학생들은 이제 필요할 때만 낮은 수준의 프로그래밍 기능들을 다루면
됩니다. 혹은 낮은 수준의 프로그래밍 기능들을 언급할 적절한 실력들이 쌓이
고 나서 보아도 됩니다.

  이처럼, 표준 라이브러리는 도구와 교사로서의 역할을 할 것입니다.

복잡성, C++와 객체지향프로그래밍(COMPLEXITY, C++,
AND OOP)

Computer: The C++ Programming Language에서 당신은 말하기를 "정상적
인 실용적인(practical) 프로그래머들은 생산성, 유지보수, 유연성과 어떠한 종
류와 스케일의 프로젝트에 대한 품질에 대해서도 놀랄만한 향상을 달성했다."
고 했습니다. 그러나 아직 몇몇 비평가들은 C++과 일반적인 OO는 여전히 너
무 복잡하고, 교정 유지보수(corrective-maintenance)문제를 일으키며, 큰 시스
템에서의 유지 문제가 있다고 합니다. 어쨌던 이러한 비평이, 당신 자신의 경
험으로 볼 때, C++로 설계하는 큰 시스템의 개발에 적용되는 것이라 생각합
니까?

Stroustrup: 저의 개인적인 경험으로, OO 설계와 OO 프로그래밍은 전통적인
절차적인 방법을 통해서 당신이 얻을 수 있는 것보다 좋은 코드를 제공할 것
입니다. 그러한 코드는 더욱 유연하고(flexible), 더욱 확장 가능하며
(extensible), 심각한 수행상의 부하(overhead)없이 유지 보수 가능한
(maintainable) 코드입니다. 위의 개인적인 의견에 반대되는 제가 좋아할 만한
강한 증거는 없습니다. 하지만, AT&T 내부에서의 몇몇 연구와 다른 연구들
은 이러한 의견을 지지합니다.

  두 요소가 쟁점을 난처하게 합니다: "객체 지향적"인 것이 정말로 무엇인지
에 대한 일반적인 동의가 없습니다. 또한 토론은 좀체 경험을 충분히 설명하
지 못합니다. "OO의 문제"의 많은 부분은 정말로 OO가 무엇인지 모르는 부
분적인 이해를 가진 사람들이 야망찬 프로젝트를 진행하는데서 비롯합니다.

  그렇다면 OO는 무엇입니까? 확실히 좋은 프로그램 모두가 객체 지향적인
것은 아니며, 객체 지향적인 모든 프로그램이 좋은 것도 아닙니다. 만약 그렇
다면 "객체 지향적"인 것이 단순히 "좋은 것"을 의미합니다. 그리고 이것은
당신이 실제적인 결정을 내려야 할 때 개념을 혼동하게 하는 하나의 추가된
전공용어(buzzword)에 불과합니다. 저는 OOP를 클래스의 상속과 가상함수(어
떤 언어에서는 메서드(method)라고 하는)를 많은 사용한 프로그램과 동일시하
곤 합니다. 이 정의는 역사적으로는 정확합니다. 왜냐하면, 클래스 계층과 가
상 함수는 그 당시 시뮬라(Simula)와 다른 언어를 구분짓는 철학이었기 때문
입니다. 사실, Smalltalk이 가장 중요하게 강조된 것은 시뮬라 유산의 이러한
관점 때문입니다.

  클래스 계층과 가상 함수에 기초하여 OO를 정의하는 것은 OO가 성공할 것
같은 곳으로의 몇 지침을 제공하기 때문에 실용적입니다. 당신은, 정확하게 같
은 형이 아니지만 공통의 인터페이스로 조작될 수 있는 객체를 위한, 구현을
공유할 수 있는 개념들의 변체(variant)를 위한 계층적인 순서를 가진 개념들
을 찾습니다. 몇 예제와 약간의 경험을 한다면, 이것이 디자인을 위한 매우 강
력한 접근의 기초가 되는 것을 알것입니다.

  그러나, 모든 개념이 자연스럽고 유용하게 계층(hierarchy)과 적합한 것은
아닙니다. 개념들간의 모든 관계가 계층적인 것도 아니고, 모든 문제에 대해
객체에 중심한 접근이 최선인 것도 아닙니다. 예를 들면, 몇 문제들은 근본적
으로 알고리즘적(algorithmic)입니다. 결론적으로, 일반 목적의 프로그래밍 언
어는 다양한 사고와 다양한 프로그래밍 스타일을 지원해야 합니다. 이러한 다
양성은 풀이해야 할 문제의 다양성과 문제 풀이의 다양성에 기인합니다. C++
은 다양한 프로그래밍 스타일을 지원합니다. 그러므로, 객체 지향 언어라기 보
다는 멀티패러다임(multiparadigm) - 이름을 꼭 붙여야 한다면 - 언어라고 불
리는 것이 더 적당합니다.

  "좋은 것" - 이해하기 쉽고, 유연하고, 효율적인 - 의 기준을 대부분 만족시
키는 디자인의 예로 전통적인 절차 코드를 사용한 재귀 경향 파서(recursive
descent parser)가(역자주: 실행 가능한 코드를 생성하는 번역기를 파서
(parser)라 하는데, 재귀 호출을 사용하며, 주로 수식을 번역하기 위해 사용한
다)  있다. 다른 예로 STL이 있습니다. 이것은 전통적인 절차적인 코드와 파
라미터적인 다형성(역자주: 이름에 의한 다형성이 아닌)에 전적으로 의존하는
컨테이너와 알고리즘을 포함한 일반 라이브러리입니다.

  저는 단 하나의 프로그래밍 패러다임만 지원하는 언어는 제한적이라는 것을
발견했습니다. 그것은 간단함(simplicity)을 사는 대신, 프로그래머에게 구속복
(straitjacket)을 입히거나, 언어의 복잡함을 응용 프로그램의 복잡함으로 전가
시킵니다. 이것은 특정 목적 언어(special-purpose language)에는 적합하지만,
범용 언어(general-purpose language)에는 적합하지 않습니다.

  저는 종종 C++를 시스템 프로그래밍으로 편향된 범용 언어라고 특징 지웁
니다. 다음을 지원하는 "더 나은 C"로 생각하는 것이죠.

  ○ 데이터 추상화

  ○ 객체 지향 프로그래밍

  ○ 일반적인 프로르래밍(generic programming).

  자연히, 이것은 하나의 접근법만을 허용하는 언어에 비해 복잡함을 증가시
킵니다. 저는 이러한 관점 - 문제 영역마다 최선인 각각의 디자인과 프로그래
밍 접근법이 있다 - 이 어떤 이들을 화나게 한다는 것을 주목했습니다. 확실
히, 모든 사람에게 혹은 모든 문제에 올바른 한가지 방법이 있다는 관점은 거
절합니다. 세계는 근본적으로 간단하다는 사실을 열정적으로 믿고 싶어하는
사람들은 제가 이것을 단지 프로그래밍 언어에 대해서만 적용한다는 사실과는
상관없이 격노(fury)합니다. 결국, 프로그래밍 언어는 시스템을 설계하기 위한
많은 도구들 중의 하나입니다.

  이러한 두 관점을 모두 해결하는 이상적인 방법은 모든 프로그래밍 스타일
을 효율적으로 지원하는 간단한 프리미티브(primitive)의 집합을 지원하는 언
어를 가지는 것입니다. 이것이 반복적으로 계속 시도되기는 했지만, 달성되지
는 - 저의 의견에 - 못했습니다.

자바와 C++(JAVA AND C++)

Computer: 위에서와 같은 논쟁이 납득할만하게 자바(Java)로 확장 가능하다
고 봅니다. Sun이 주장하기를 700,000명의 자바 프로그래머가 있다고 합니다
(비교해서 C++ 프로그래머는 1,500,000명 정도). 당신은 C++가 C와 호환될 필
요도 없으며, 자바가 당신이 고안한 언어가 아니라는 주장을 고집합니다. 이러
한 질문을 1000번 이상 받았다면 당신은 무슨 말을 할 것입니까?

Stroustrup: 오늘날, 저는 항상 자바에 대해서 질문 받습니다. 그리고 항상
대답하기는 어렵습니다. 제가 만약 약간 부정적인 것을 말한다면, 무례한 것으
로 느껴집니다. 제가 만약 약간 긍정적인 것을 말한다면, 자바를 둘러싼 상업
적인 공간(commercial hype)에 뛰어드는 것 같고, 자바 공동체의 부분으로부
터 흘러나오는 불운한 반 C++ 선전(the unfortunate anti-C++ propaganda)에
뛰어드는 느낌이 들기 때문입니다.

  저는 사람들에게 상업적인 라이벌 상태가 아닌, 자신이 개발해야 하는 디자
인 기준에 따라 두 개의 언어 중 적절한 것을 선택하라고 권장합니다. 저는
C++을 위한 디자인 표준(cirteria)을 The Design and Evolution of C++에서
개괄해 놓았습니다. 그리고 자바는 이러한 기준을 만족시키는 시작조차 못하
고 있습니다. 자바를 만병통치약으로 생각하지 않고 프로그래밍 언어의 하나
로 생각하는 것은 좋습니다. 결국, C++는 자바의 디자인 목표와 완벽하게 일
치하지 않습니다. 그러나, 자바가 유일한 프로그래밍 언어로써 진급된다면, 자
바의 결함과 제한은 심각하게 될 것입니다.

  C++을 개발하는데 적용된 기준이 자바에는 적용되지 않는 몇 가지 예가 아
래에 있습니다.

  ○ 서로 다른 언어로부터 쓰여진 프로그램 부분들을 효과적으로 조합하는

    능력

  ○ 다양한 디자인 스타일과 프로그램 스타일

  ○ 기정의 타입(built-in type)과 맞먹는 효율을 가진 사용자 정의 타입

  ○ 기정의 타입과 사용자 정의 타입의 일관된 처리(treatment)

  ○ STL처럼 실행시 부담없는 일반 컨테이터(generic container)를 사용하는
능력

  프로그래머들이 우아하고 효율적인 코드를 작성할 수 있도록 제가 C++을
디자인했습니다. 많은 응용프로그램을 위해서, 우아함과 효율성을 타협하기를
원하지 않을 때 C++은 여전히 최상의 선택입니다.

  저는 어떻게 사람들이 "프로그래머"를 세는지 의문입니다. 학생이 프로그래
머입니까? PC에 컴파일러를 가진 사람은 모두 프로그래머입니까? 저는 C++
을 위해서 인용된 숫자를 이해합니다. 그것은 보수적이고 그럴듯합니다. 그것
은 지난 해 실제 팔린 금액을 위한 C++ 프로그램의 수를 적절히 근사화한 것
입니다. 저는 Sun에서 주장하는 자바 프로그래머의 수가 같은 근거인지 의문
입니다. 부수적으로, 컴파일러 판매, 책 판매, C++ 수강생 수 등의 확실한 수
에 근거해서, 제가 추측하기로 C++ 프로그래머는 매년 10%∼20%증가하고 있
습니다.

  그리고 저는 자바 팬(fan)이 아닙니다. 저는 과대 선전(hype)을 싫어합니다.
저는 프로그래머가 아닌 사람에게 프로그래밍 툴을 판매하는 시장 전략을 싫
어합니다. 저는 독점적인 언어를 싫어합니다. 그리고 저는 많은 프로그래밍 언
어를 좋아합니다. 기술적인 측면에서, 자바는 저에게 다른 언어에서 받았던 것
처럼, "와 깔끔한데!"란 인상을 주지 못했습니다. 자바는 제가 생각하기에 제
한적인 형태로 다른 유명한 언어의 기능들을 제공합니다.

  자바는 많은 것을 C++로부터 빌려 왔습니다. 하지만 그렇게 많이는 아니며,
원하는 만큼의 이해와 맛(taste)도 빌려오지 못했습니다. 자바를 위한 중요한
약속들을 지키기 위해선, 자바는 성장해야 합니다. 이러한 전개는 C++보다 간
단하게 라는 자바의 주장과 타협할지도 모릅니다. 그러나 저의 추측에 그러한
노력은 현재의 것보다 더 좋은 자바 언어를 만들 것입니다. 현재, 자바는 언어
의 특징들과 "표준" 라이브러리를 꽤(quite a pace) 축적하고 있는 것처럼 보
입니다. 저는 자바 버전의 템플릿이 나타날 것을 기대하고 있습니다.

  자바와 그의 공동체가 성숙해가면서, 활기있고 활기있게 하는 철학
(live-and-let-live philosophy)을 희망적으로 채택할 것입니다. 이것은 자바가
프로 프로그래머들의 소중한 도구(tool chest)중의 한 언어로 자리잡게 할 것
입니다.

도구(TOOLS)

Computer: 지난 몇 년간 어떤 방법으로 시스템이 변화했습니까? 그리고
C++의 활동범위(arena)와 일반적인 시스템 디자인에 무엇이 더 필요합니까?
C++가 비교적 성장한 이 시점에서, 사용할 수 있는 C++ 툴(tool)들을 어떻게
평가하겠습니까? 그리고 여전히 개선되어야 하는 것은 어떤 것들입니까?

Stroustrup: 먼저, 컴파일러, 디버거, 프로파일러(profiler)(역주: 프로그램의
성능 분석을 위한 도구), 데이터베이스 인터페이스, GUI 빌더, CAD 툴과 ISO
표준을 지원하는 기타 기본 툴들을 살펴보고자 합니다. 예를 들면, 저는 데이
터베이스 질의의 결과를 STL 컨테이너 혹은 적절한 객체 형의 istream으로
얻고 싶습니다. 툴 벤더(vendor)는 좋은 출발을 하였습니다. 하지만, 컴파일러
나 다른 소스 코드 분석기에 의존적인 작업을 너무 많이 하였습니다.

  제가 리스트한 기본 도구들은 무엇이 바뀐지에 대한 부분적인 대답에 불과
합니다. 과거 몇 년 동안, 많은 수의 프로그래머들이 훌륭한 도구와 시스템 기
능을 제공하는 인터페이스 코드에 의존해 왔습니다. 이러한 도구들은 지루하
고, 에러 발생이 쉬운 작업으로부터 프로그래머들을 구제하는데 필수적입니다.
하지만, 프로그래머가(혹은 고용인이) 툴 제공자의 포로가 되는 위험에 처해
있습니다.

  종종 이러한 도구들은 전통적인 프로그래밍 언어에 비해 더욱 복잡하며, 표
준은 거의 없습니다. 저는 독점성이(nonproprietary) 없는 도구와 라이브러리
를 추천하고 싶습니다. 짧은 안목으로 보면, 예를 들면 10년 정도, 많은 그러
한 표준은 형식적인 ISO 나 IEEE의 표준보다 산업계의 표준역할을 합니다.
하지만, 건강한 소프트웨어 산업을 위해, 중요 인터페이스는 잘 정의되고 대중
이 사용할 수 있는 것이 필수입니다.

  COM이나 CORBA같은 시스템 레벨 객체를 위한 표준의 중요성이 증가하
는 가운데, C++가 그러한 것들과 깨끗하고(clean), 잘 문서화되어, 사용하기
쉽게 결합될 수 있는 것은 특별히 중요합니다. 불행하게도, 그러한 "표준 작
업" - 모두에게 이로운 - 은 누구에게도 짧은 안목의 이점을 제공하지 않기
때문에 소흘히 됩니다.

  저는 사용 가능한 C++ 툴들을 평가하지 않겠습니다. 그것들은 매우 훌륭하
며, 다른 언어의 어떤 툴보다 더 좋습니다. 그러나, 프로그래머로서, 저는 당연
히 더 좋은 품질의 툴들을 바랍니다. 개인적으로, C++ 소스코드를 분석하는
더 좋은 툴들을 기대하고 있습니다. 저는 또한 C++ 컴파일러 벤더들이 신제
품을 개발하는 것보다, 자사 컴파일러의 품질과 수행 능력을 향상시키는데 예
산을 조금 더 투자하기를 기대합니다. 컴파일러의 실제적인 성능향상은 새로
운 버전의 완전한 C++ 컴파일러를 위한 시간보다 다소 저렴합니다. 그러나,
중요한 사용자들이 적응 테스팅(conformance testing)이나 벤치마킹
(benchmarking)을 요구하는 것을 시작하지 않는다면, 벤더들이 광고상 좋게
보이는 것을 위해 자원을 사용할 것을 느낍니다.

미래(THE FUTURE)

Computer: 저는 당신이 C++표준화 작업에 많이 참여한 것으로 알고있습니
다. 하지만, 현재 당신이 참여한 다른 프로젝트에는 어떤 것이 있으며, C++을
발전시키기 위해 어떤 작업을 계속할 것입니까?

Stroustrup: 나는 매우 큰 프로젝트를 성공적으로 완성하고 나서 고통받고
있습니다. ISO C++ 표준이 완성되었습니다. The C++ Programming
Language의 제 3판에는 표준 C++이 무엇을 제공하는지 어떻게 가장 잘 사용
하는지 진지한 프로그래머들에게 알리고 있습니다. 그리고 Design and
Evolution은 C++에 장착된(shaped) 디자인 결정들을 문서화하고 있습니다. 해
야할 것들이 아직 많습니다. 그러나 전시간 언어 디자이너를 필요로 하는 것
은 아무것도 없습니다. 이제 가능한 변화들에 집중하기 보다는, 코드를 작성함
으로써 C++의 힘과 유연성을 즐길 시간입니다.

  그것과는 별도로, 저는 몇 가지 사실들을 배울 기회를 얻었습니다. 몇 새로
운 언어와 큰 사업에서 소프트웨어가 어떻게 사용되는지에 관한 것이 그것입
니다. 그리고 분산 컴퓨팅에서 몇 실험들을 계획중입니다.¤

Bjarne Stroustrup 은 AT&T의 대규모 프로그래밍 연구센터의 소장입니다;
bs@research.att.com; http://www.research.att.com/~bs/

  실습문제

1.


67. 표준화 유감

67. 표준화 유감(regret
to the standard)

  이 책의 원고를 쓰는 작업이 진행중인 동안 C++의 표준이 마침내 정해졌다
는 소식을 들었다(ISO에서 1997년 11월에 정식으로 표준을 정했지만, 필자는
98년 봄에야 소식을 들었다). 하지만, 필자가 생각했던 몇 부분이 빠져 있어
조금은 유감이었다. 하지만, C++은 계속해서 발전해 나갈 것이고, 후에 이러
한 사항이 C++에 추가될 것을 확신한다.

  후에 추가될 것이라 예상되는 - 하지만 지금은 어떤 컴파일러도 지원하지
않는 - 사항을 아래에 정리하였다.

  using의 기능 개선: 객체의 범위 해결

  특정 객체의 멤버를 빈번하게 접근해야 한다면, 멤버 지정 연산자 점(.)과
활살표(->)는 여간 번거로운 것이 아니다. 예를 들어 아래와 같은 클래스를
생각해 보자.

#include <stdio.h>

class CTest {

public:

    int i,j;

    CTest(int ti,int tj) { i=ti; j=tj; }

    void Add() { printf("%d\n", i+j); }

    void Subtract() { printf("%d\n", i-j); }

    void Multiply() { printf("%d\n", i*j); }

    void Divide() { printf("%d\n", i/j); }

};//class CTest

void main() {

    CTest* t;

    CTest* u;

    t=new CTest(5,2);

    u=new CTest(50,20);

    t->Add();

    t->Subtract();

    t->Multiply();

    t->Divide();

    u->Add();

    u->Subtract();

    u->Multiply();

    u->Divide();

    delete t;

    delete u;

}

  프로그램의 실행 결과는

7

3

10

2

70

30

1000

2

이다. 위의 예에서 객체 t와 객체 u의 멤버를 계속해서 접근하고 있다. 이러한
경우 다음의 문법으로 대치한다면 프로그램의 가독성을 증가시킬 것이다.

#include <stdio.h>

class CTest {

public:

    int i,j;

    CTest(int ti,int tj) { i=ti; j=tj; }

    void Add() { printf("%d\n", i+j); }

    void Subtract() { printf("%d\n", i-j); }

    void Multiply() { printf("%d\n", i*j); }

    void Divide() { printf("%d\n", i/j); }

};//class CTest

void main() {

    CTest* t;

    CTest* u;

    t=new CTest(5,2);

    u=new CTest(50,20);

    using *t {//이 문장은 현 컴파일러는 지원하지 않는다. 어쩌면 영원히

               //지원되지 않을 수도 있다.

        Add();

        Subtract();

        Multiply();

        Divide();

    }//using t

    using *u {

        Add();

        Subtract();

        Multiply();

        Divide();

    }//using *u

    delete t;

    delete u;

}

  using은 이미 표준에 추가된 키워드이므로 키워드가 추가되는 부담은 없다.
또한 namespace와의 충돌 문제는 범위 해결 규칙(scope rule)으로 해결할 수
있다. using의 범위 해결 규칙은 다음과 같다.

    □ 지역 범위의 객체를 먼저 본다. 객체가 없는 경우, namespace를

    본다.

    □ 객체와 namespace가 충돌하는 경우, 객체가 우선된다. 이를 해결

    하기 위해서 범위 해결사(::)를 이용할 수 있다.

  매크로 배열(macro array)

  '참조표'의 예에서 보았듯이, 배열은 변수로서 사용될 목적이 아니라, 단지
참조하기 위해서 사용되는 경우가 빈번하다. 함수의 사상(mapping)을 구현한
예를 다시 적어 보자.

#include <stdio.h>

int mapping[5][2]={{34,876},{125,56},{18,12},{345,11},{22,23}};

void main() {

    int i,n;

    scanf("%d",&n);

    for (i=0;i<5;++i)

        if (n==mapping[i][0]) {

            printf("%d\n",mapping[i][1]);

            break;

        }//if

}//main

  위의 예에서 mapping[]이란 2차원 배열은 단지 값을 참조하기 위해 사용될
뿐이다. 하지만, mapping[]은 전역 변수로 할당되어 메모리를 차지하고 있다.
이것은 명확한 오버헤드(overhead)이다. 만약 mapping[]이 #define의 매크로
상수처럼 간주된다면, 즉 mapping[i][j]가 컴파일 전에 배열의 값으로 치환 -
예를 들면 mapping[0][1]은 876으로 - 된다면, mapping은 더 이상 메모리를
차지하지 않게 된다. 어떻게 이것을 정의할 것인가? 해결해야 할 사항은 다음
과 같다.

    □ 이러한 용도로 사용된 배열이 LValue가 되었을 때 이를 검사할

    수 있어야 한다.

    □ 이러한 배열은 - 이것을 매크로 배열이라 하자 - 컴파일 전에 처

    리되어야 하며, C++에 새로운 키워드가 추가되는 오버헤드는 없어야

    한다.

  기억나는가? 바로 inline을 사용하는 것이다. 수정된 소스는 아래와 같다. 물
론 이 코드는 컴파일러에서 컴파일 되지는 않는다.

#include <stdio.h>

inline int mapping[5][2]={{34,876},{125,56},{18,12},{345,11},{22,23}};

void main() {

    int i,n;

    scanf("%d",&n);

    for (i=0;i<5;++i)

        if (n==mapping[i][0]) {

            printf("%d\n",mapping[i][1]);

            break;

        }//if

}//main

   정적 변수와 정적 함수의 참조

  클래스 안에서 선언된 정적 멤버(데이터 멤버이든 멤버 함수이든)는 메모리
에 오직 1개의 인스턴스가 존재한다. 또한 정적 멤버는 클래스 객체의 존재
유무와는 상관없이 프로그램이 실행되면서, 메모리에 할당되어 프로그램이 종
료할 때까지 존재하게 된다.

  정적 멤버는 항상 메모리에 할당되어 있으므로, 특별하게 private로 선언되
어 있지 않는 한, 전역 가시 범위를 가진다. 즉, 아무 곳에서나 클래스의 정적
멤버를 참조할 수 있다. 아래의 예를 보자.

#include <iostream.h>

class CTest {

public:

  static int s;

  static void IncS() { ++s; }

};//class CTest

int CTest::s=100;

void main() {

    CTest::IncS();

    cout << CTest::s << endl;

}

출력 결과는

101

이다. 위의 예에서 정적 멤버를 참조하기 위해서 범위 해결사(scope resolver)
를 사용하고 있다. 범위 해결사는 이 밖에도, 상위 클래스의 호출 등에 사용된
다. s는 CTest의 어떠한 객체의 멤버도 아니므로, 그리고 CTest가 클래스이
지 객체가 아니므로,

CTest.s

라고 사용하는 것은 문법적 에러이다. 하지만, 문맥에 의해 CTest가 클래스인
것이 확실하므로, CTest::s라고 쓰거나, CTest.s라고 쓰거나, 구분할 방법은
존재한다.

  프로그래머 입장에서도 CTest가 클래스인 것이 확실하므로, CTest.s라고
쓰는 것은 어떠한 혼동도 유발하지 않는다. 그러므로, CTest::s 형식의 문법
외에 CTest.s의 문법도 지원하는 것이 바람직하다.

  Java는 CTest.s의 문법만 지원한다. 하지만, C++는 훨씬 유동적인 언어이므
로, CTest::s와 CTest.s의 두 가지 모두를 지원해야 할 것이다. 필자는 개인적
으로 이러한 사항이 후에 C++의 표준에 추가될 것을 확신한다.

web posted by seojt@comeng.ce.kyungpook.ac.kr